Skip to content

Implement POST /v1/swap/quote#37

Closed
findolor wants to merge 2 commits into2026-02-16-implement-order-deployfrom
2026-02-16-implement-swap-quote
Closed

Implement POST /v1/swap/quote#37
findolor wants to merge 2 commits into2026-02-16-implement-order-deployfrom
2026-02-16-implement-swap-quote

Conversation

@findolor
Copy link
Collaborator

@findolor findolor commented Feb 16, 2026

Chained PRs

Motivation

See issues:

The swap quote endpoint was stubbed with todo!(). Users need a way to get a price quote before executing a swap, showing the estimated input cost and IO ratio for a desired output amount. The flat swap.rs file also needed to be refactored into a module directory to match the order module pattern and support the upcoming calldata implementation.

Solution

Refactored swap.rs into a swap/ module directory (mod.rs, quote.rs, calldata.rs) and implemented the quote endpoint using the rain.orderbook library's take_orders pipeline:

  • SwapDataSource trait with get_orders_for_pair and build_candidates_for_pair — the latter delegates to the library's build_take_order_candidates_for_pair, which fetches quotes in parallel, filters by pair direction, removes failed/zero-capacity quotes
  • simulate_buy_over_candidates for capacity-aware multi-leg pricing — sorts candidates by best price, greedily fills across multiple orders, and returns accurate total_input/total_output
  • Blended IO ratio computed as total_input / total_output across all simulation legs, giving an accurate average price even when liquidity is split across orders
  • Uses Float::max_positive_value() as price cap for quotes (no filtering — show all available liquidity)
  • 10 unit tests covering success, multi-leg fills, partial fills, best-ratio selection, no liquidity, no candidates, invalid input, query failure, auth, and client init failure

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 #20

Summary by CodeRabbit

Release Notes

  • New Features

    • Swap quote endpoint enhanced with multi-leg quote support, partial fill handling, and blended ratio calculations for improved pricing accuracy
  • Refactor

    • Reorganized swap module structure with improved data source abstraction for better code organization
  • Chores

    • Added math float dependency for enhanced mathematical operations

Refactor swap from flat file to module directory and implement the quote
endpoint using the library's take_orders pipeline: orders are fetched,
candidates built with pair-direction filtering, then simulated via
simulate_buy_over_candidates for accurate multi-leg pricing.
@findolor findolor self-assigned this Feb 16, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

The PR restructures the swap routes module from a single file into a modular architecture, introduces a SwapDataSource trait abstraction, implements the swap quote endpoint with full business logic including order fetching and price simulation, and adds a new rain-math-float dependency to support float operations.

Changes

Cohort / File(s) Summary
Dependencies
Cargo.toml
Added rain-math-float dependency from path reference.
Swap Module Architecture
src/routes/swap/mod.rs
Introduced SwapDataSource trait with async methods for order fetching and candidate building; implemented concrete RaindexSwapDataSource struct; exposed routes registration; added test fixtures with MockSwapDataSource for testing support.
Swap Endpoints
src/routes/swap/calldata.rs, src/routes/swap/quote.rs
Separated endpoint implementations from module file; calldata.rs (41 lines) registers POST /v1/swap/calldata as placeholder; quote.rs (272 lines) fully implements POST /v1/swap/quote with order fetching, candidate building, buy simulation, and ratio computation logic; comprehensive unit tests cover success paths, multi-leg quotes, partial fills, and error cases.
Cleanup
src/routes/swap.rs
Removed original monolithic swap routes file; functionality migrated to modular structure.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as API Handler<br/>(post_swap_quote)
    participant DS as SwapDataSource
    participant RaindexClient
    participant FloatOps as Float Operations

    Client->>API: POST /v1/swap/quote
    activate API
    
    API->>DS: get_orders_for_pair(input, output)
    activate DS
    DS->>RaindexClient: fetch orders
    RaindexClient-->>DS: Vec<RaindexOrder>
    DS-->>API: Result<Vec<Order>>
    deactivate DS
    
    API->>DS: build_candidates_for_pair(orders, input, output)
    activate DS
    DS->>RaindexClient: build candidates from orders
    RaindexClient-->>DS: Vec<TakeOrderCandidate>
    DS-->>API: Result<Vec<Candidate>>
    deactivate DS
    
    API->>FloatOps: parse output_amount
    FloatOps-->>API: buy_target (float)
    
    API->>FloatOps: simulate buys over candidates
    FloatOps-->>API: total_input, total_output
    
    API->>FloatOps: compute blended ratio
    FloatOps-->>API: estimated_io_ratio
    
    API-->>Client: Json<SwapQuoteResponse>
    deactivate API
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • JuaniRios
  • hardyjosh

Poem

🐰 The swap routes hop and play,

Modular now, a cleaner way,

Orders fetch, candidates build,

Quotes computed, wallets filled,

Float math dances—hooray! 💫

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR fully addresses issue #20 requirements: implements POST /v1/swap/quote endpoint, accepts SwapQuoteRequest and returns SwapQuoteResponse, integrates with the rain.orderbook quoting pipeline, and validates request fields.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the swap quote endpoint: dependency addition, module restructuring (swap.rs to swap/ directory), SwapDataSource trait, quote/calldata endpoints, and comprehensive tests.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into 2026-02-16-implement-order-deploy
Title check ✅ Passed The pull request title 'Implement POST /v1/swap/quote' clearly and specifically describes the main change: implementation of the swap quote endpoint.

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

✨ 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 2026-02-16-implement-swap-quote

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
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@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

🤖 Fix all issues with AI agents
In `@src/routes/swap/calldata.rs`:
- Around line 34-37: The current handler using raindex.run_with_client(move
|_client| async move { todo!() }) will panic at runtime; either remove this
route registration from routes() until implemented, or replace the todo!() with
an async branch that returns a proper ApiError mapped to HTTP 501 Not
Implemented (so the .map_err(ApiError::from)? path still composes). Locate the
call inside calldata.rs (the raindex.run_with_client closure) and either delete
the route registration in routes() (so it is not reachable) or change the
closure to return an Err(ApiError) that represents "Not Implemented" instead of
calling todo!(). Ensure you reference raindex.run_with_client and the
surrounding .map_err(ApiError::from)? when implementing the fix.

In `@src/routes/swap/quote.rs`:
- Around line 108-114: The response currently returns the requested
output_amount instead of the actual filled amount; update SwapQuoteResponse to
include a new field (e.g., filled_amount or actual_output) and populate it where
the quote is constructed so callers can distinguish requested vs filled units
(see SwapQuoteResponse and the quote construction that returns
formatted_input/formatted_ratio); update any serializers/tests (notably
test_process_swap_quote_partial_fill) and API docs to ensure the new field is
returned and clearly documented as the actual filled amount when liquidity was
insufficient.
- Line 10: Remove the unused import of the std::ops::Div trait at the top of the
file; it is not used anywhere (the code calls the inherent method Float::div(),
which returns a Result), so delete the line `use std::ops::Div;` to clean up the
import list and fix the unused-import warning.
🧹 Nitpick comments (5)
src/routes/swap/quote.rs (2)

89-96: Consider guarding against zero total_output before division.

While sim.legs.is_empty() being false should imply total_output > 0, a defensive check would prevent a division-by-zero error if simulate_buy_over_candidates ever returns legs with zero output due to a bug or edge case upstream.

Suggested defensive check
     if sim.legs.is_empty() {
         return Err(ApiError::NotFound("no valid quotes available".into()));
     }
 
+    // Defensive: ensure total_output is non-zero before division
+    let zero = Float::parse("0".to_string()).map_err(|e| {
+        tracing::error!(error = %e, "failed to parse zero");
+        ApiError::Internal("internal error".into())
+    })?;
+    if sim.total_output == zero {
+        return Err(ApiError::NotFound("no valid quotes available".into()));
+    }
+
     let blended_ratio = sim.total_input.div(sim.total_output).map_err(|e| {

139-233: Solid unit test suite covering key scenarios.

Consider adding edge-case tests for output_amount values of "0" and negative numbers (e.g., "-100") to verify behavior at the boundaries.

src/routes/swap/mod.rs (3)

36-73: Implementation looks correct.

The order filter correctly scopes to active: true with matching input/output tokens. Error handling consistently maps to ApiError::Internal with structured logging. The delegation to build_take_order_candidates_for_pair with None, None for the optional params is clean.

One note: the None arguments passed to build_take_order_candidates_for_pair (line 66) and get_orders (line 52) are opaque without context. Brief inline comments indicating what these represent would aid readability.


93-118: Mock get_orders_for_pair discards the original error variant.

Line 107 always returns ApiError::Internal("failed to query orders") regardless of what error variant self.orders contains. This means tests using MockSwapDataSource can't distinguish between different error types propagated from get_orders_for_pair.

This is minor for current tests but could become a gap if you later add tests that need to verify specific error propagation (e.g., NotFound vs Internal).

Suggestion: propagate the actual error
     async fn get_orders_for_pair(
         &self,
         _input_token: Address,
         _output_token: Address,
     ) -> Result<Vec<RaindexOrder>, ApiError> {
         match &self.orders {
             Ok(orders) => Ok(orders.clone()),
-            Err(_) => Err(ApiError::Internal("failed to query orders".into())),
+            Err(e) => Err(e.clone()),
         }
     }

This requires ApiError to implement Clone. If that's not feasible, you could store a closure or a factory function instead.


75-76: Wildcard re-exports may leak internal items.

pub use calldata::* and pub use quote::* re-export everything public from those submodules. Currently this is fine since only the handler functions are pub, but if someone later adds a pub helper to quote.rs or calldata.rs, it will be unintentionally exported from the swap module.

Consider explicit re-exports for tighter API control:

-pub use calldata::*;
-pub use quote::*;
+pub use calldata::post_swap_calldata;
+pub use quote::post_swap_quote;

Comment on lines +34 to +37
raindex
.run_with_client(move |_client| async move { todo!() })
.await
.map_err(ApiError::from)?
Copy link

@coderabbitai coderabbitai bot Feb 16, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

todo!() will panic at runtime on a registered route.

This route is registered in routes() (see src/routes/swap/mod.rs line 79) and is reachable by any authenticated user. The todo!() macro causes an unrecoverable panic, which will crash the request handler (and potentially the worker thread depending on Rocket's configuration).

Either:

  1. Don't register the route until it's implemented, or
  2. Return a proper error response (e.g., 501 Not Implemented).
Option 1: Remove from routes until ready

In src/routes/swap/mod.rs:

 pub fn routes() -> Vec<Route> {
-    rocket::routes![quote::post_swap_quote, calldata::post_swap_calldata]
+    rocket::routes![quote::post_swap_quote]
 }
Option 2: Return a proper error instead of panicking
     raindex
-        .run_with_client(move |_client| async move { todo!() })
-        .await
-        .map_err(ApiError::from)?
+        .run_with_client(move |_client| async move {
+            Err::<Json<SwapCalldataResponse>, ApiError>(
+                ApiError::Internal("calldata endpoint not yet implemented".into()),
+            )
+        })
+        .await
+        .map_err(ApiError::from)?
🤖 Prompt for AI Agents
In `@src/routes/swap/calldata.rs` around lines 34 - 37, The current handler using
raindex.run_with_client(move |_client| async move { todo!() }) will panic at
runtime; either remove this route registration from routes() until implemented,
or replace the todo!() with an async branch that returns a proper ApiError
mapped to HTTP 501 Not Implemented (so the .map_err(ApiError::from)? path still
composes). Locate the call inside calldata.rs (the raindex.run_with_client
closure) and either delete the route registration in routes() (so it is not
reachable) or change the closure to return an Err(ApiError) that represents "Not
Implemented" instead of calling todo!(). Ensure you reference
raindex.run_with_client and the surrounding .map_err(ApiError::from)? when
implementing the fix.

Copy link
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 now as we are not live yet. We are doing these one by one.

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! That makes sense for incremental development. I'll note that the endpoint is pending implementation and won't flag similar patterns while you're working through the endpoints one by one.

(•ᴗ•)

use rain_orderbook_common::take_orders::simulate_buy_over_candidates;
use rocket::serde::json::Json;
use rocket::State;
use std::ops::Div;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the quote.rs file to see the import and any usage
cat -n src/routes/swap/quote.rs | head -100

Repository: ST0x-Technology/st0x.rest.api

Length of output: 4411


🏁 Script executed:

# Search for any usage of Div trait or the Div operator in quote.rs
rg -n "Div|/" src/routes/swap/quote.rs -A 2

Repository: ST0x-Technology/st0x.rest.api

Length of output: 731


🏁 Script executed:

# Find the Float type definition to understand its div method
rg -rn "type Float|struct Float|impl.*Float" --type rs | head -20

Repository: ST0x-Technology/st0x.rest.api

Length of output: 99


🏁 Script executed:

# Check if std::ops::Div is used anywhere in the codebase
rg -n "std::ops::Div|<.*as.*Div>" --type rs

Repository: ST0x-Technology/st0x.rest.api

Length of output: 99


🏁 Script executed:

# Get the full quote.rs file
wc -l src/routes/swap/quote.rs

Repository: ST0x-Technology/st0x.rest.api

Length of output: 101


🏁 Script executed:

# Read the complete file
cat src/routes/swap/quote.rs

Repository: ST0x-Technology/st0x.rest.api

Length of output: 10200


🏁 Script executed:

# Search for Float type and its div method in all rust files
rg -n "impl.*Float|fn div" --type-list

Repository: ST0x-Technology/st0x.rest.api

Length of output: 462


🏁 Script executed:

# Search more broadly for Float and div
rg -n "Float.*div|div.*Float"

Repository: ST0x-Technology/st0x.rest.api

Length of output: 55


Remove unused std::ops::Div import.

The std::ops::Div trait is never used in this file. Line 93 calls Float::div(), which is an inherent method returning Result, not the trait method. The import can be safely removed.

🤖 Prompt for AI Agents
In `@src/routes/swap/quote.rs` at line 10, Remove the unused import of the
std::ops::Div trait at the top of the file; it is not used anywhere (the code
calls the inherent method Float::div(), which returns a Result), so delete the
line `use std::ops::Div;` to clean up the import list and fix the unused-import
warning.

Comment on lines +108 to +114
Ok(SwapQuoteResponse {
input_token: req.input_token,
output_token: req.output_token,
output_amount: req.output_amount,
estimated_input: formatted_input,
estimated_io_ratio: formatted_ratio,
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Partial fill response echoes the requested output_amount, not the actual filled amount.

When liquidity is insufficient (as shown in test_process_swap_quote_partial_fill), the response returns the requested output_amount ("100") even though only 30 units can actually be filled. The caller has no way to know that the quote only covers a fraction of the requested amount.

Consider adding a field like actual_output or filled_amount to SwapQuoteResponse, or at minimum document that output_amount is the requested amount, not a guaranteed fill.

🤖 Prompt for AI Agents
In `@src/routes/swap/quote.rs` around lines 108 - 114, The response currently
returns the requested output_amount instead of the actual filled amount; update
SwapQuoteResponse to include a new field (e.g., filled_amount or actual_output)
and populate it where the quote is constructed so callers can distinguish
requested vs filled units (see SwapQuoteResponse and the quote construction that
returns formatted_input/formatted_ratio); update any serializers/tests (notably
test_process_swap_quote_partial_fill) and API docs to ensure the new field is
returned and clearly documented as the actual filled amount when liquidity was
insufficient.

@findolor findolor changed the title swap: implement POST /v1/swap/quote with capacity-aware simulation Implement POST /v1/swap/quote Feb 17, 2026
@findolor findolor mentioned this pull request Feb 17, 2026
4 tasks
@findolor findolor requested review from 0xgleb and hardyjosh February 17, 2026 07:13
@findolor
Copy link
Collaborator Author

Closing - already implemented in main branch.

@findolor findolor closed this Feb 25, 2026
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.

1 participant