Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ clap = { version = "4.5.58", features = ["derive"] }
reqwest = { version = "0.13.2", features = ["json"] }
rain_orderbook_js_api = { path = "lib/rain.orderbook/crates/js_api", default-features = false }
rain_orderbook_common = { path = "lib/rain.orderbook/crates/common", default-features = false }
rain_orderbook_bindings = { path = "lib/rain.orderbook/crates/bindings", default-features = false }
rain-math-float = { path = "lib/rain.orderbook/lib/rain.interpreter/lib/rain.interpreter.interface/lib/rain.math.float/crates/float" }
async-trait = "0.1"
wasm-bindgen = "=0.2.100"

[dev-dependencies]
Expand Down
83 changes: 0 additions & 83 deletions src/routes/swap.rs

This file was deleted.

42 changes: 42 additions & 0 deletions src/routes/swap/calldata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::auth::AuthenticatedKey;
use crate::error::{ApiError, ApiErrorResponse};
use crate::fairings::{GlobalRateLimit, TracingSpan};
use crate::types::swap::{SwapCalldataRequest, SwapCalldataResponse};
use rocket::serde::json::Json;
use rocket::State;
use tracing::Instrument;

#[utoipa::path(
post,
path = "/v1/swap/calldata",
tag = "Swap",
security(("basicAuth" = [])),
request_body = SwapCalldataRequest,
responses(
(status = 200, description = "Swap calldata", body = SwapCalldataResponse),
(status = 400, description = "Bad request", body = ApiErrorResponse),
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
(status = 429, description = "Rate limited", body = ApiErrorResponse),
(status = 500, description = "Internal server error", body = ApiErrorResponse),
)
)]
#[post("/calldata", data = "<request>")]
pub async fn post_swap_calldata(
_global: GlobalRateLimit,
_key: AuthenticatedKey,
shared_raindex: &State<crate::raindex::SharedRaindexProvider>,
span: TracingSpan,
request: Json<SwapCalldataRequest>,
) -> Result<Json<SwapCalldataResponse>, ApiError> {
let req = request.into_inner();
async move {
tracing::info!(body = ?req, "request received");
let raindex = shared_raindex.read().await;
raindex
.run_with_client(move |_client| async move { todo!() })
.await
.map_err(ApiError::from)?
Comment on lines +35 to +38
Copy link

@coderabbitai coderabbitai bot Feb 20, 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 any request to this registered endpoint.

This endpoint is wired into routes() in mod.rs (Line 79) and documented in OpenAPI, so authenticated clients can reach it. The todo!() macro causes an unrecoverable panic, which will crash the request handler. Either return a proper ApiError (e.g., 501 Not Implemented) or remove the route from routes() until the implementation is ready.

Proposed fix: return a proper error instead of panicking
         raindex
-            .run_with_client(move |_client| async move { todo!() })
+            .run_with_client(move |_client| async move {
+                Err::<Json<SwapCalldataResponse>, ApiError>(ApiError::Internal(
+                    "swap calldata endpoint not yet implemented".into(),
+                ))
+            })
             .await
             .map_err(ApiError::from)?

Alternatively, remove the route from the router until it's implemented:

 pub fn routes() -> Vec<Route> {
-    rocket::routes![quote::post_swap_quote, calldata::post_swap_calldata]
+    rocket::routes![quote::post_swap_quote]
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/swap/calldata.rs` around lines 35 - 38, The closure passed to
raindex.run_with_client currently contains todo!() which will panic at runtime;
replace the todo!() with a proper error return instead of panicking (for example
return an ApiError representing 501 Not Implemented or another appropriate
ApiError variant) so the call to
raindex.run_with_client(...).await.map_err(ApiError::from)? yields a handled
error rather than a panic; alternatively remove the route registration that
references this handler in routes() until you implement the function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Don't worry we'll be implementing this downstream.

Copy link

Choose a reason for hiding this comment

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

@findolor, understood! I'll leave this for your future implementation. 👍


🧠 Learnings used
Learnt from: findolor
Repo: ST0x-Technology/st0x.rest.api PR: 38
File: src/routes/swap/mod.rs:115-129
Timestamp: 2026-02-17T09:36:00.616Z
Learning: In the swap calldata endpoint (Rust, `src/routes/swap/mod.rs`), when the response includes a non-empty `approvals` array, the two-phase flow is intentional: the client performs the approval transaction first, then calls the endpoint again to retrieve the actual swap calldata. In the approval response, `to` is set to `approval.spender()` to signal the eventual target, and `data` is `Bytes::new()` (empty) to indicate "no transaction yet." This design is preferred over using `Address::ZERO` for clarity.

}
.instrument(span.0)
.await
}
118 changes: 118 additions & 0 deletions src/routes/swap/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
mod calldata;
mod quote;

use crate::error::ApiError;
use alloy::primitives::Address;
use async_trait::async_trait;
use rain_orderbook_common::raindex_client::orders::{
GetOrdersFilters, GetOrdersTokenFilter, RaindexOrder,
};
use rain_orderbook_common::raindex_client::RaindexClient;
use rain_orderbook_common::take_orders::{
build_take_order_candidates_for_pair, TakeOrderCandidate,
};
use rocket::Route;

#[async_trait(?Send)]
pub(crate) trait SwapDataSource {
async fn get_orders_for_pair(
&self,
input_token: Address,
output_token: Address,
) -> Result<Vec<RaindexOrder>, ApiError>;

async fn build_candidates_for_pair(
&self,
orders: &[RaindexOrder],
input_token: Address,
output_token: Address,
) -> Result<Vec<TakeOrderCandidate>, ApiError>;
}

pub(crate) struct RaindexSwapDataSource<'a> {
pub client: &'a RaindexClient,
}

#[async_trait(?Send)]
impl<'a> SwapDataSource for RaindexSwapDataSource<'a> {
async fn get_orders_for_pair(
&self,
input_token: Address,
output_token: Address,
) -> Result<Vec<RaindexOrder>, ApiError> {
let filters = GetOrdersFilters {
active: Some(true),
tokens: Some(GetOrdersTokenFilter {
inputs: Some(vec![input_token]),
outputs: Some(vec![output_token]),
}),
..Default::default()
};
self.client
.get_orders(None, Some(filters), None)
.await
.map_err(|e| {
tracing::error!(error = %e, "failed to query orders for pair");
ApiError::Internal("failed to query orders".into())
})
}

async fn build_candidates_for_pair(
&self,
orders: &[RaindexOrder],
input_token: Address,
output_token: Address,
) -> Result<Vec<TakeOrderCandidate>, ApiError> {
build_take_order_candidates_for_pair(orders, input_token, output_token, None, None)
.await
.map_err(|e| {
tracing::error!(error = %e, "failed to build order candidates");
ApiError::Internal("failed to build order candidates".into())
})
}
}

pub use calldata::*;
pub use quote::*;

pub fn routes() -> Vec<Route> {
rocket::routes![quote::post_swap_quote, calldata::post_swap_calldata]
}

#[cfg(test)]
pub(crate) mod test_fixtures {
use super::SwapDataSource;
use crate::error::ApiError;
use alloy::primitives::Address;
use async_trait::async_trait;
use rain_orderbook_common::raindex_client::orders::RaindexOrder;
use rain_orderbook_common::take_orders::TakeOrderCandidate;

pub struct MockSwapDataSource {
pub orders: Result<Vec<RaindexOrder>, ApiError>,
pub candidates: Vec<TakeOrderCandidate>,
}

#[async_trait(?Send)]
impl SwapDataSource for MockSwapDataSource {
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())),
}
}

async fn build_candidates_for_pair(
&self,
_orders: &[RaindexOrder],
_input_token: Address,
_output_token: Address,
) -> Result<Vec<TakeOrderCandidate>, ApiError> {
Ok(self.candidates.clone())
}
}
}
Loading