From 991d42df0d94f95f79dd8b0be25faae907841a1e Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 16 Feb 2026 11:58:00 +0100 Subject: [PATCH 01/10] order: extract OrderDataSource trait for testable get_order pipeline Introduce an async trait to abstract the three raindex calls in get_order (get_orders, get_quotes, get_trades_list), enabling unit tests that exercise determine_order_type and build_order_detail with real deserialized RaindexOrder/RaindexTrade instances via a mock data source. Adds seven tests covering the happy path, not-found, empty trades, query failure, default order type, 401 without auth, and 500 on client init failure. --- Cargo.toml | 1 + src/routes/order.rs | 463 +++++++++++++++++++++++++++++++++++++++++++- src/test_helpers.rs | 7 +- 3 files changed, 460 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2a2ae7f..8579f35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ utoipa = { version = "5", features = ["rocket_extras"] } utoipa-swagger-ui = { version = "9", features = ["rocket"] } tokio = { version = "1", features = ["full"] } alloy = { version = "=1.0.12", default-features = false, features = ["std", "serde"] } +async-trait = "0.1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } tracing-appender = "0.2" diff --git a/src/routes/order.rs b/src/routes/order.rs index 22311d6..a72c6d6 100644 --- a/src/routes/order.rs +++ b/src/routes/order.rs @@ -1,15 +1,85 @@ use crate::auth::AuthenticatedKey; use crate::error::{ApiError, ApiErrorResponse}; use crate::fairings::{GlobalRateLimit, TracingSpan}; -use crate::types::common::ValidatedFixedBytes; +use crate::types::common::{TokenRef, ValidatedFixedBytes}; use crate::types::order::{ CancelOrderRequest, CancelOrderResponse, DeployDcaOrderRequest, DeployOrderResponse, - DeploySolverOrderRequest, OrderDetail, + DeploySolverOrderRequest, OrderDetail, OrderDetailsInfo, OrderTradeEntry, OrderType, }; +use alloy::primitives::B256; +use async_trait::async_trait; +use rain_orderbook_common::parsed_meta::ParsedMeta; +use rain_orderbook_common::raindex_client::orders::{GetOrdersFilters, RaindexOrder}; +use rain_orderbook_common::raindex_client::trades::RaindexTrade; +use rain_orderbook_common::raindex_client::RaindexClient; use rocket::serde::json::Json; use rocket::{Route, State}; use tracing::Instrument; +#[async_trait(?Send)] +trait OrderDataSource { + async fn get_orders_by_hash(&self, hash: B256) -> Result, ApiError>; + async fn get_order_io_ratio(&self, order: &RaindexOrder) -> String; + async fn get_order_trades(&self, order: &RaindexOrder) -> Vec; +} + +struct RaindexOrderDataSource<'a> { + client: &'a RaindexClient, +} + +#[async_trait(?Send)] +impl<'a> OrderDataSource for RaindexOrderDataSource<'a> { + async fn get_orders_by_hash(&self, hash: B256) -> Result, ApiError> { + let filters = GetOrdersFilters { + order_hash: Some(hash), + ..Default::default() + }; + self.client + .get_orders(None, Some(filters), None) + .await + .map_err(|e| { + tracing::error!(error = %e, "failed to query orders"); + ApiError::Internal("failed to query orders".into()) + }) + } + + async fn get_order_io_ratio(&self, order: &RaindexOrder) -> String { + match order.get_quotes(None, None).await { + Ok(quotes) => quotes + .first() + .and_then(|q| q.data.as_ref()) + .map(|d| d.formatted_ratio.clone()) + .unwrap_or_default(), + Err(e) => { + tracing::warn!(error = %e, "failed to fetch order quotes"); + String::new() + } + } + } + + async fn get_order_trades(&self, order: &RaindexOrder) -> Vec { + match order.get_trades_list(None, None, None).await { + Ok(t) => t, + Err(e) => { + tracing::warn!(error = %e, "failed to fetch order trades"); + vec![] + } + } + } +} + +async fn process_get_order(ds: &dyn OrderDataSource, hash: B256) -> Result { + let orders = ds.get_orders_by_hash(hash).await?; + let order = orders + .into_iter() + .next() + .ok_or_else(|| ApiError::NotFound("order not found".into()))?; + let io_ratio = ds.get_order_io_ratio(&order).await; + let trades = ds.get_order_trades(&order).await; + let order_type = determine_order_type(&order); + build_order_detail(&order, order_type, &io_ratio, &trades) +} + #[utoipa::path( post, path = "/v1/order/dca", @@ -104,10 +174,15 @@ pub async fn get_order( ) -> Result, ApiError> { async move { tracing::info!(order_hash = ?order_hash, "request received"); - raindex - .run_with_client(move |_client| async move { todo!() }) + let hash = order_hash.0; + let detail = raindex + .run_with_client(move |client| async move { + let ds = RaindexOrderDataSource { client: &client }; + process_get_order(&ds, hash).await + }) .await - .map_err(ApiError::from)? + .map_err(ApiError::from)??; + Ok(Json(detail)) } .instrument(span.0) .await @@ -148,6 +223,90 @@ pub async fn post_order_cancel( .await } +fn determine_order_type(order: &RaindexOrder) -> OrderType { + for meta in order.parsed_meta() { + if let ParsedMeta::DotrainGuiStateV1(gui_state) = meta { + if gui_state + .selected_deployment + .to_lowercase() + .contains("dca") + { + return OrderType::Dca; + } + } + } + OrderType::Solver +} + +fn build_order_detail( + order: &RaindexOrder, + order_type: OrderType, + io_ratio: &str, + trades: &[RaindexTrade], +) -> Result { + let inputs = order.inputs_list().items(); + let outputs = order.outputs_list().items(); + + let input = inputs.first().ok_or_else(|| { + tracing::error!("order has no input vaults"); + ApiError::Internal("order has no input vaults".into()) + })?; + let output = outputs.first().ok_or_else(|| { + tracing::error!("order has no output vaults"); + ApiError::Internal("order has no output vaults".into()) + })?; + + let input_token_info = input.token(); + let output_token_info = output.token(); + + let trade_entries: Vec = trades.iter().map(map_trade).collect(); + + let created_at: u64 = order + .timestamp_added() + .try_into() + .unwrap_or(0); + + Ok(OrderDetail { + order_hash: order.order_hash(), + owner: order.owner(), + order_details: OrderDetailsInfo { + type_: order_type, + io_ratio: io_ratio.to_string(), + }, + input_token: TokenRef { + address: input_token_info.address(), + symbol: input_token_info.symbol().unwrap_or_default(), + decimals: input_token_info.decimals(), + }, + output_token: TokenRef { + address: output_token_info.address(), + symbol: output_token_info.symbol().unwrap_or_default(), + decimals: output_token_info.decimals(), + }, + input_vault_id: input.vault_id(), + output_vault_id: output.vault_id(), + input_vault_balance: input.formatted_balance(), + output_vault_balance: output.formatted_balance(), + io_ratio: io_ratio.to_string(), + created_at, + orderbook_id: order.orderbook(), + trades: trade_entries, + }) +} + +fn map_trade(trade: &RaindexTrade) -> OrderTradeEntry { + let timestamp: u64 = trade.timestamp().try_into().unwrap_or(0); + let tx = trade.transaction(); + OrderTradeEntry { + id: trade.id().to_string(), + tx_hash: tx.id(), + input_amount: trade.input_vault_balance_change().formatted_amount(), + output_amount: trade.output_vault_balance_change().formatted_amount(), + timestamp, + sender: tx.from(), + } +} + pub fn routes() -> Vec { rocket::routes![ post_order_dca, @@ -156,3 +315,297 @@ pub fn routes() -> Vec { post_order_cancel ] } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_helpers::{ + basic_auth_header, mock_invalid_raindex_config, seed_api_key, TestClientBuilder, + }; + use alloy::primitives::Address; + use rocket::http::{Header, Status}; + use serde_json::json; + + fn stub_raindex_client() -> serde_json::Value { + json!({ + "orderbook_yaml": { + "documents": ["version: 4\nnetworks:\n base:\n rpcs:\n - https://mainnet.base.org\n chain-id: 8453\n currency: ETH\nsubgraphs:\n base: https://example.com/sg\norderbooks:\n base:\n address: 0xd2938e7c9fe3597f78832ce780feb61945c377d7\n network: base\n subgraph: base\n deployment-block: 0\ndeployers:\n base:\n address: 0xC1A14cE2fd58A3A2f99deCb8eDd866204eE07f8D\n network: base\n"], + "profile": "strict" + } + }) + } + + fn order_json() -> serde_json::Value { + let rc = stub_raindex_client(); + json!({ + "raindexClient": rc, + "chainId": 8453, + "id": "0x0000000000000000000000000000000000000000000000000000000000000001", + "orderBytes": "0x01", + "orderHash": "0x000000000000000000000000000000000000000000000000000000000000abcd", + "owner": "0x0000000000000000000000000000000000000001", + "orderbook": "0xd2938e7c9fe3597f78832ce780feb61945c377d7", + "active": true, + "timestampAdded": "0x000000000000000000000000000000000000000000000000000000006553f100", + "meta": null, + "parsedMeta": [], + "rainlang": null, + "transaction": { + "id": "0x0000000000000000000000000000000000000000000000000000000000000099", + "from": "0x0000000000000000000000000000000000000001", + "blockNumber": "0x0000000000000000000000000000000000000000000000000000000000000001", + "timestamp": "0x000000000000000000000000000000000000000000000000000000006553f100" + }, + "tradesCount": 0, + "inputs": [{ + "raindexClient": rc, + "chainId": 8453, + "vaultType": "input", + "id": "0x01", + "owner": "0x0000000000000000000000000000000000000001", + "vaultId": "0x0000000000000000000000000000000000000000000000000000000000000001", + "balance": "0x0000000000000000000000000000000000000000000000000000000000000001", + "formattedBalance": "1.000000", + "token": { + "chainId": 8453, + "id": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "name": "USD Coin", + "symbol": "USDC", + "decimals": 6 + }, + "orderbook": "0xd2938e7c9fe3597f78832ce780feb61945c377d7", + "ordersAsInputs": [], + "ordersAsOutputs": [] + }], + "outputs": [{ + "raindexClient": rc, + "chainId": 8453, + "vaultType": "output", + "id": "0x02", + "owner": "0x0000000000000000000000000000000000000001", + "vaultId": "0x0000000000000000000000000000000000000000000000000000000000000002", + "balance": "0xffffffff00000000000000000000000000000000000000000000000000000005", + "formattedBalance": "0.500000000000000000", + "token": { + "chainId": 8453, + "id": "0x4200000000000000000000000000000000000006", + "address": "0x4200000000000000000000000000000000000006", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18 + }, + "orderbook": "0xd2938e7c9fe3597f78832ce780feb61945c377d7", + "ordersAsInputs": [], + "ordersAsOutputs": [] + }] + }) + } + + fn trade_json() -> serde_json::Value { + json!({ + "id": "0x0000000000000000000000000000000000000000000000000000000000000042", + "orderHash": "0x000000000000000000000000000000000000000000000000000000000000abcd", + "transaction": { + "id": "0x0000000000000000000000000000000000000000000000000000000000000088", + "from": "0x0000000000000000000000000000000000000002", + "blockNumber": "0x0000000000000000000000000000000000000000000000000000000000000064", + "timestamp": "0x000000000000000000000000000000000000000000000000000000006553f4e8" + }, + "inputVaultBalanceChange": { + "type": "takeOrder", + "vaultId": "0x0000000000000000000000000000000000000000000000000000000000000001", + "token": { + "chainId": 8453, + "id": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "name": "USD Coin", + "symbol": "USDC", + "decimals": 6 + }, + "amount": "0xffffffff00000000000000000000000000000000000000000000000000000005", + "formattedAmount": "0.500000", + "newBalance": "0xffffffff0000000000000000000000000000000000000000000000000000000f", + "formattedNewBalance": "1.500000", + "oldBalance": "0x0000000000000000000000000000000000000000000000000000000000000001", + "formattedOldBalance": "1.000000", + "timestamp": "0x000000000000000000000000000000000000000000000000000000006553f4e8", + "transaction": { + "id": "0x0000000000000000000000000000000000000000000000000000000000000088", + "from": "0x0000000000000000000000000000000000000002", + "blockNumber": "0x0000000000000000000000000000000000000000000000000000000000000064", + "timestamp": "0x000000000000000000000000000000000000000000000000000000006553f4e8" + }, + "orderbook": "0xd2938e7c9fe3597f78832ce780feb61945c377d7" + }, + "outputVaultBalanceChange": { + "type": "takeOrder", + "vaultId": "0x0000000000000000000000000000000000000000000000000000000000000002", + "token": { + "chainId": 8453, + "id": "0x4200000000000000000000000000000000000006", + "address": "0x4200000000000000000000000000000000000006", + "name": "Wrapped Ether", + "symbol": "WETH", + "decimals": 18 + }, + "amount": "0x0000000000000000000000000000000000000000000000000000000000000001", + "formattedAmount": "-0.250000000000000000", + "newBalance": "0x0000000000000000000000000000000000000000000000000000000000000001", + "formattedNewBalance": "0.250000000000000000", + "oldBalance": "0xffffffff00000000000000000000000000000000000000000000000000000005", + "formattedOldBalance": "0.500000000000000000", + "timestamp": "0x000000000000000000000000000000000000000000000000000000006553f4e8", + "transaction": { + "id": "0x0000000000000000000000000000000000000000000000000000000000000088", + "from": "0x0000000000000000000000000000000000000002", + "blockNumber": "0x0000000000000000000000000000000000000000000000000000000000000064", + "timestamp": "0x000000000000000000000000000000000000000000000000000000006553f4e8" + }, + "orderbook": "0xd2938e7c9fe3597f78832ce780feb61945c377d7" + }, + "timestamp": "0x000000000000000000000000000000000000000000000000000000006553f4e8", + "orderbook": "0xd2938e7c9fe3597f78832ce780feb61945c377d7" + }) + } + + fn mock_order() -> RaindexOrder { + serde_json::from_value(order_json()).expect("deserialize mock RaindexOrder") + } + + fn mock_trade() -> RaindexTrade { + serde_json::from_value(trade_json()).expect("deserialize mock RaindexTrade") + } + + struct MockOrderDataSource { + orders: Result, ApiError>, + trades: Vec, + io_ratio: String, + } + + #[async_trait(?Send)] + impl OrderDataSource for MockOrderDataSource { + async fn get_orders_by_hash(&self, _hash: B256) -> Result, ApiError> { + match &self.orders { + Ok(orders) => Ok(orders.clone()), + Err(_) => Err(ApiError::Internal("failed to query orders".into())), + } + } + async fn get_order_io_ratio(&self, _order: &RaindexOrder) -> String { + self.io_ratio.clone() + } + async fn get_order_trades(&self, _order: &RaindexOrder) -> Vec { + self.trades.clone() + } + } + + fn test_hash() -> B256 { + "0x000000000000000000000000000000000000000000000000000000000000abcd" + .parse() + .unwrap() + } + + #[rocket::async_test] + async fn test_process_get_order_success() { + let ds = MockOrderDataSource { + orders: Ok(vec![mock_order()]), + trades: vec![mock_trade()], + io_ratio: "1.5".into(), + }; + let detail = process_get_order(&ds, test_hash()).await.unwrap(); + + assert_eq!(detail.order_hash, test_hash()); + assert_eq!( + detail.owner, + "0x0000000000000000000000000000000000000001" + .parse::
() + .unwrap() + ); + assert_eq!(detail.input_token.symbol, "USDC"); + assert_eq!(detail.output_token.symbol, "WETH"); + assert_eq!(detail.input_vault_balance, "1.000000"); + assert_eq!(detail.output_vault_balance, "0.500000000000000000"); + assert_eq!(detail.io_ratio, "1.5"); + assert_eq!(detail.order_details.type_, OrderType::Solver); + assert_eq!(detail.order_details.io_ratio, "1.5"); + assert_eq!(detail.created_at, 1700000000); + assert_eq!(detail.trades.len(), 1); + assert_eq!(detail.trades[0].input_amount, "0.500000"); + assert_eq!(detail.trades[0].output_amount, "-0.250000000000000000"); + assert_eq!(detail.trades[0].timestamp, 1700001000); + } + + #[rocket::async_test] + async fn test_process_get_order_not_found() { + let ds = MockOrderDataSource { + orders: Ok(vec![]), + trades: vec![], + io_ratio: String::new(), + }; + let result = process_get_order(&ds, test_hash()).await; + assert!(matches!(result, Err(ApiError::NotFound(_)))); + } + + #[rocket::async_test] + async fn test_process_get_order_empty_trades() { + let ds = MockOrderDataSource { + orders: Ok(vec![mock_order()]), + trades: vec![], + io_ratio: "2.0".into(), + }; + let detail = process_get_order(&ds, test_hash()).await.unwrap(); + assert!(detail.trades.is_empty()); + assert_eq!(detail.io_ratio, "2.0"); + } + + #[rocket::async_test] + async fn test_process_get_order_query_failure() { + let ds = MockOrderDataSource { + orders: Err(ApiError::Internal("failed to query orders".into())), + trades: vec![], + io_ratio: String::new(), + }; + let result = process_get_order(&ds, test_hash()).await; + assert!(matches!(result, Err(ApiError::Internal(_)))); + } + + #[rocket::async_test] + async fn test_determine_order_type_solver_default() { + let order = mock_order(); + assert_eq!(determine_order_type(&order), OrderType::Solver); + } + + #[rocket::async_test] + async fn test_get_order_401_without_auth() { + let client = TestClientBuilder::new().build().await; + let response = client + .get("/v1/order/0x000000000000000000000000000000000000000000000000000000000000abcd") + .dispatch() + .await; + assert_eq!(response.status(), Status::Unauthorized); + } + + #[rocket::async_test] + async fn test_get_order_500_when_client_init_fails() { + let config = mock_invalid_raindex_config().await; + let client = TestClientBuilder::new() + .raindex_config(config) + .build() + .await; + let (key_id, secret) = seed_api_key(&client).await; + let header = basic_auth_header(&key_id, &secret); + let response = client + .get("/v1/order/0x000000000000000000000000000000000000000000000000000000000000abcd") + .header(Header::new("Authorization", header)) + .dispatch() + .await; + assert_eq!(response.status(), Status::InternalServerError); + let body: serde_json::Value = + serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(body["error"]["code"], "INTERNAL_ERROR"); + assert_eq!( + body["error"]["message"], + "failed to initialize orderbook client" + ); + } +} diff --git a/src/test_helpers.rs b/src/test_helpers.rs index 01022f5..d84c6bb 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -37,12 +37,7 @@ impl TestClientBuilder { self } - pub(crate) fn raindex_registry_url(mut self, url: impl Into) -> Self { - self.raindex_registry_url = Some(url.into()); - self - } - - pub(crate) async fn build(self) -> Client { +pub(crate) async fn build(self) -> Client { let id = uuid::Uuid::new_v4(); let pool = crate::db::init(&format!("sqlite:file:{id}?mode=memory&cache=shared")) .await From 0fa963f5a9e4bd8747fb859f904b06fdfbbc07c0 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 16 Feb 2026 12:10:44 +0100 Subject: [PATCH 02/10] order: return full quotes from OrderDataSource instead of io_ratio string --- src/routes/order.rs | 57 ++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/routes/order.rs b/src/routes/order.rs index a72c6d6..4eb604b 100644 --- a/src/routes/order.rs +++ b/src/routes/order.rs @@ -9,6 +9,7 @@ use crate::types::order::{ use alloy::primitives::B256; use async_trait::async_trait; use rain_orderbook_common::parsed_meta::ParsedMeta; +use rain_orderbook_common::raindex_client::order_quotes::RaindexOrderQuote; use rain_orderbook_common::raindex_client::orders::{GetOrdersFilters, RaindexOrder}; use rain_orderbook_common::raindex_client::trades::RaindexTrade; use rain_orderbook_common::raindex_client::RaindexClient; @@ -19,7 +20,7 @@ use tracing::Instrument; #[async_trait(?Send)] trait OrderDataSource { async fn get_orders_by_hash(&self, hash: B256) -> Result, ApiError>; - async fn get_order_io_ratio(&self, order: &RaindexOrder) -> String; + async fn get_order_quotes(&self, order: &RaindexOrder) -> Vec; async fn get_order_trades(&self, order: &RaindexOrder) -> Vec; } @@ -43,16 +44,12 @@ impl<'a> OrderDataSource for RaindexOrderDataSource<'a> { }) } - async fn get_order_io_ratio(&self, order: &RaindexOrder) -> String { + async fn get_order_quotes(&self, order: &RaindexOrder) -> Vec { match order.get_quotes(None, None).await { - Ok(quotes) => quotes - .first() - .and_then(|q| q.data.as_ref()) - .map(|d| d.formatted_ratio.clone()) - .unwrap_or_default(), + Ok(quotes) => quotes, Err(e) => { tracing::warn!(error = %e, "failed to fetch order quotes"); - String::new() + vec![] } } } @@ -74,7 +71,12 @@ async fn process_get_order(ds: &dyn OrderDataSource, hash: B256) -> Result serde_json::Value { + json!({ + "pair": { "pairName": "USDC/WETH", "inputIndex": 0, "outputIndex": 0 }, + "blockNumber": 1, + "data": { + "maxOutput": "0x0000000000000000000000000000000000000000000000000000000000000001", + "formattedMaxOutput": "1", + "maxInput": "0x0000000000000000000000000000000000000000000000000000000000000002", + "formattedMaxInput": "2", + "ratio": "0x0000000000000000000000000000000000000000000000000000000000000002", + "formattedRatio": formatted_ratio, + "inverseRatio": "0xffffffff00000000000000000000000000000000000000000000000000000005", + "formattedInverseRatio": "0.5" + }, + "success": true, + "error": null + }) + } + + fn mock_quote(formatted_ratio: &str) -> RaindexOrderQuote { + serde_json::from_value(quote_json(formatted_ratio)).expect("deserialize mock quote") + } + struct MockOrderDataSource { orders: Result, ApiError>, trades: Vec, - io_ratio: String, + quotes: Vec, } #[async_trait(?Send)] @@ -491,8 +516,8 @@ mod tests { Err(_) => Err(ApiError::Internal("failed to query orders".into())), } } - async fn get_order_io_ratio(&self, _order: &RaindexOrder) -> String { - self.io_ratio.clone() + async fn get_order_quotes(&self, _order: &RaindexOrder) -> Vec { + self.quotes.clone() } async fn get_order_trades(&self, _order: &RaindexOrder) -> Vec { self.trades.clone() @@ -510,7 +535,7 @@ mod tests { let ds = MockOrderDataSource { orders: Ok(vec![mock_order()]), trades: vec![mock_trade()], - io_ratio: "1.5".into(), + quotes: vec![mock_quote("1.5")], }; let detail = process_get_order(&ds, test_hash()).await.unwrap(); @@ -540,7 +565,7 @@ mod tests { let ds = MockOrderDataSource { orders: Ok(vec![]), trades: vec![], - io_ratio: String::new(), + quotes: vec![], }; let result = process_get_order(&ds, test_hash()).await; assert!(matches!(result, Err(ApiError::NotFound(_)))); @@ -551,7 +576,7 @@ mod tests { let ds = MockOrderDataSource { orders: Ok(vec![mock_order()]), trades: vec![], - io_ratio: "2.0".into(), + quotes: vec![mock_quote("2.0")], }; let detail = process_get_order(&ds, test_hash()).await.unwrap(); assert!(detail.trades.is_empty()); @@ -563,7 +588,7 @@ mod tests { let ds = MockOrderDataSource { orders: Err(ApiError::Internal("failed to query orders".into())), trades: vec![], - io_ratio: String::new(), + quotes: vec![], }; let result = process_get_order(&ds, test_hash()).await; assert!(matches!(result, Err(ApiError::Internal(_)))); From 4e332a1e6b6434c41047728cb1a70c0d5c645ead Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 16 Feb 2026 12:19:02 +0100 Subject: [PATCH 03/10] order: split into directory module with one file per route --- src/routes/order/cancel.rs | 42 ++++ src/routes/order/deploy_dca.rs | 41 ++++ src/routes/order/deploy_solver.rs | 41 ++++ src/routes/{order.rs => order/get_order.rs} | 201 ++++---------------- src/routes/order/mod.rs | 75 ++++++++ 5 files changed, 232 insertions(+), 168 deletions(-) create mode 100644 src/routes/order/cancel.rs create mode 100644 src/routes/order/deploy_dca.rs create mode 100644 src/routes/order/deploy_solver.rs rename src/routes/{order.rs => order/get_order.rs} (76%) create mode 100644 src/routes/order/mod.rs diff --git a/src/routes/order/cancel.rs b/src/routes/order/cancel.rs new file mode 100644 index 0000000..f4c5cbe --- /dev/null +++ b/src/routes/order/cancel.rs @@ -0,0 +1,42 @@ +use crate::auth::AuthenticatedKey; +use crate::error::{ApiError, ApiErrorResponse}; +use crate::fairings::{GlobalRateLimit, TracingSpan}; +use crate::types::order::{CancelOrderRequest, CancelOrderResponse}; +use rocket::serde::json::Json; +use rocket::State; +use tracing::Instrument; + +#[utoipa::path( + post, + path = "/v1/order/cancel", + tag = "Order", + security(("basicAuth" = [])), + request_body = CancelOrderRequest, + responses( + (status = 200, description = "Cancel order result", body = CancelOrderResponse), + (status = 400, description = "Bad request", body = ApiErrorResponse), + (status = 401, description = "Unauthorized", body = ApiErrorResponse), + (status = 429, description = "Rate limited", body = ApiErrorResponse), + (status = 404, description = "Order not found", body = ApiErrorResponse), + (status = 500, description = "Internal server error", body = ApiErrorResponse), + ) +)] +#[post("/cancel", data = "")] +pub async fn post_order_cancel( + _global: GlobalRateLimit, + _key: AuthenticatedKey, + raindex: &State, + span: TracingSpan, + request: Json, +) -> Result, ApiError> { + let req = request.into_inner(); + async move { + tracing::info!(body = ?req, "request received"); + raindex + .run_with_client(move |_client| async move { todo!() }) + .await + .map_err(ApiError::from)? + } + .instrument(span.0) + .await +} diff --git a/src/routes/order/deploy_dca.rs b/src/routes/order/deploy_dca.rs new file mode 100644 index 0000000..682a89a --- /dev/null +++ b/src/routes/order/deploy_dca.rs @@ -0,0 +1,41 @@ +use crate::auth::AuthenticatedKey; +use crate::error::{ApiError, ApiErrorResponse}; +use crate::fairings::{GlobalRateLimit, TracingSpan}; +use crate::types::order::{DeployDcaOrderRequest, DeployOrderResponse}; +use rocket::serde::json::Json; +use rocket::State; +use tracing::Instrument; + +#[utoipa::path( + post, + path = "/v1/order/dca", + tag = "Order", + security(("basicAuth" = [])), + request_body = DeployDcaOrderRequest, + responses( + (status = 200, description = "DCA order deployment result", body = DeployOrderResponse), + (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("/dca", data = "")] +pub async fn post_order_dca( + _global: GlobalRateLimit, + _key: AuthenticatedKey, + raindex: &State, + span: TracingSpan, + request: Json, +) -> Result, ApiError> { + let req = request.into_inner(); + async move { + tracing::info!(body = ?req, "request received"); + raindex + .run_with_client(move |_client| async move { todo!() }) + .await + .map_err(ApiError::from)? + } + .instrument(span.0) + .await +} diff --git a/src/routes/order/deploy_solver.rs b/src/routes/order/deploy_solver.rs new file mode 100644 index 0000000..1115276 --- /dev/null +++ b/src/routes/order/deploy_solver.rs @@ -0,0 +1,41 @@ +use crate::auth::AuthenticatedKey; +use crate::error::{ApiError, ApiErrorResponse}; +use crate::fairings::{GlobalRateLimit, TracingSpan}; +use crate::types::order::{DeployOrderResponse, DeploySolverOrderRequest}; +use rocket::serde::json::Json; +use rocket::State; +use tracing::Instrument; + +#[utoipa::path( + post, + path = "/v1/order/solver", + tag = "Order", + security(("basicAuth" = [])), + request_body = DeploySolverOrderRequest, + responses( + (status = 200, description = "Solver order deployment result", body = DeployOrderResponse), + (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("/solver", data = "")] +pub async fn post_order_solver( + _global: GlobalRateLimit, + _key: AuthenticatedKey, + raindex: &State, + span: TracingSpan, + request: Json, +) -> Result, ApiError> { + let req = request.into_inner(); + async move { + tracing::info!(body = ?req, "request received"); + raindex + .run_with_client(move |_client| async move { todo!() }) + .await + .map_err(ApiError::from)? + } + .instrument(span.0) + .await +} diff --git a/src/routes/order.rs b/src/routes/order/get_order.rs similarity index 76% rename from src/routes/order.rs rename to src/routes/order/get_order.rs index 4eb604b..9238bb7 100644 --- a/src/routes/order.rs +++ b/src/routes/order/get_order.rs @@ -1,70 +1,19 @@ +use super::{OrderDataSource, RaindexOrderDataSource}; use crate::auth::AuthenticatedKey; use crate::error::{ApiError, ApiErrorResponse}; use crate::fairings::{GlobalRateLimit, TracingSpan}; use crate::types::common::{TokenRef, ValidatedFixedBytes}; use crate::types::order::{ - CancelOrderRequest, CancelOrderResponse, DeployDcaOrderRequest, DeployOrderResponse, - DeploySolverOrderRequest, OrderDetail, OrderDetailsInfo, OrderTradeEntry, OrderType, + OrderDetail, OrderDetailsInfo, OrderTradeEntry, OrderType, }; use alloy::primitives::B256; -use async_trait::async_trait; use rain_orderbook_common::parsed_meta::ParsedMeta; -use rain_orderbook_common::raindex_client::order_quotes::RaindexOrderQuote; -use rain_orderbook_common::raindex_client::orders::{GetOrdersFilters, RaindexOrder}; +use rain_orderbook_common::raindex_client::orders::RaindexOrder; use rain_orderbook_common::raindex_client::trades::RaindexTrade; -use rain_orderbook_common::raindex_client::RaindexClient; use rocket::serde::json::Json; -use rocket::{Route, State}; +use rocket::State; use tracing::Instrument; -#[async_trait(?Send)] -trait OrderDataSource { - async fn get_orders_by_hash(&self, hash: B256) -> Result, ApiError>; - async fn get_order_quotes(&self, order: &RaindexOrder) -> Vec; - async fn get_order_trades(&self, order: &RaindexOrder) -> Vec; -} - -struct RaindexOrderDataSource<'a> { - client: &'a RaindexClient, -} - -#[async_trait(?Send)] -impl<'a> OrderDataSource for RaindexOrderDataSource<'a> { - async fn get_orders_by_hash(&self, hash: B256) -> Result, ApiError> { - let filters = GetOrdersFilters { - order_hash: Some(hash), - ..Default::default() - }; - self.client - .get_orders(None, Some(filters), None) - .await - .map_err(|e| { - tracing::error!(error = %e, "failed to query orders"); - ApiError::Internal("failed to query orders".into()) - }) - } - - async fn get_order_quotes(&self, order: &RaindexOrder) -> Vec { - match order.get_quotes(None, None).await { - Ok(quotes) => quotes, - Err(e) => { - tracing::warn!(error = %e, "failed to fetch order quotes"); - vec![] - } - } - } - - async fn get_order_trades(&self, order: &RaindexOrder) -> Vec { - match order.get_trades_list(None, None, None).await { - Ok(t) => t, - Err(e) => { - tracing::warn!(error = %e, "failed to fetch order trades"); - vec![] - } - } - } -} - async fn process_get_order(ds: &dyn OrderDataSource, hash: B256) -> Result { let orders = ds.get_orders_by_hash(hash).await?; let order = orders @@ -82,74 +31,6 @@ async fn process_get_order(ds: &dyn OrderDataSource, hash: B256) -> Result, - span: TracingSpan, - request: Json, -) -> Result, ApiError> { - let req = request.into_inner(); - async move { - tracing::info!(body = ?req, "request received"); - raindex - .run_with_client(move |_client| async move { todo!() }) - .await - .map_err(ApiError::from)? - } - .instrument(span.0) - .await -} - -#[utoipa::path( - post, - path = "/v1/order/solver", - tag = "Order", - security(("basicAuth" = [])), - request_body = DeploySolverOrderRequest, - responses( - (status = 200, description = "Solver order deployment result", body = DeployOrderResponse), - (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("/solver", data = "")] -pub async fn post_order_solver( - _global: GlobalRateLimit, - _key: AuthenticatedKey, - raindex: &State, - span: TracingSpan, - request: Json, -) -> Result, ApiError> { - let req = request.into_inner(); - async move { - tracing::info!(body = ?req, "request received"); - raindex - .run_with_client(move |_client| async move { todo!() }) - .await - .map_err(ApiError::from)? - } - .instrument(span.0) - .await -} - #[utoipa::path( get, path = "/v1/order/{order_hash}", @@ -190,41 +71,6 @@ pub async fn get_order( .await } -#[utoipa::path( - post, - path = "/v1/order/cancel", - tag = "Order", - security(("basicAuth" = [])), - request_body = CancelOrderRequest, - responses( - (status = 200, description = "Cancel order result", body = CancelOrderResponse), - (status = 400, description = "Bad request", body = ApiErrorResponse), - (status = 401, description = "Unauthorized", body = ApiErrorResponse), - (status = 429, description = "Rate limited", body = ApiErrorResponse), - (status = 404, description = "Order not found", body = ApiErrorResponse), - (status = 500, description = "Internal server error", body = ApiErrorResponse), - ) -)] -#[post("/cancel", data = "")] -pub async fn post_order_cancel( - _global: GlobalRateLimit, - _key: AuthenticatedKey, - raindex: &State, - span: TracingSpan, - request: Json, -) -> Result, ApiError> { - let req = request.into_inner(); - async move { - tracing::info!(body = ?req, "request received"); - raindex - .run_with_client(move |_client| async move { todo!() }) - .await - .map_err(ApiError::from)? - } - .instrument(span.0) - .await -} - fn determine_order_type(order: &RaindexOrder) -> OrderType { for meta in order.parsed_meta() { if let ParsedMeta::DotrainGuiStateV1(gui_state) = meta { @@ -309,22 +155,18 @@ fn map_trade(trade: &RaindexTrade) -> OrderTradeEntry { } } -pub fn routes() -> Vec { - rocket::routes![ - post_order_dca, - post_order_solver, - get_order, - post_order_cancel - ] -} - #[cfg(test)] mod tests { use super::*; + use crate::error::ApiError; use crate::test_helpers::{ basic_auth_header, mock_invalid_raindex_config, seed_api_key, TestClientBuilder, }; - use alloy::primitives::Address; + use alloy::primitives::{Address, B256}; + use async_trait::async_trait; + use rain_orderbook_common::raindex_client::order_quotes::RaindexOrderQuote; + use rain_orderbook_common::raindex_client::orders::RaindexOrder; + use rain_orderbook_common::raindex_client::trades::RaindexTrade; use rocket::http::{Header, Status}; use serde_json::json; @@ -502,6 +344,17 @@ mod tests { serde_json::from_value(quote_json(formatted_ratio)).expect("deserialize mock quote") } + fn mock_failed_quote() -> RaindexOrderQuote { + serde_json::from_value(json!({ + "pair": { "pairName": "USDC/WETH", "inputIndex": 0, "outputIndex": 0 }, + "blockNumber": 1, + "data": null, + "success": false, + "error": "quote failed" + })) + .expect("deserialize mock failed quote") + } + struct MockOrderDataSource { orders: Result, ApiError>, trades: Vec, @@ -583,6 +436,18 @@ mod tests { assert_eq!(detail.io_ratio, "2.0"); } + #[rocket::async_test] + async fn test_process_get_order_failed_quote() { + let ds = MockOrderDataSource { + orders: Ok(vec![mock_order()]), + trades: vec![], + quotes: vec![mock_failed_quote()], + }; + let detail = process_get_order(&ds, test_hash()).await.unwrap(); + assert_eq!(detail.io_ratio, "-"); + assert_eq!(detail.order_details.io_ratio, "-"); + } + #[rocket::async_test] async fn test_process_get_order_query_failure() { let ds = MockOrderDataSource { diff --git a/src/routes/order/mod.rs b/src/routes/order/mod.rs new file mode 100644 index 0000000..3439b6b --- /dev/null +++ b/src/routes/order/mod.rs @@ -0,0 +1,75 @@ +mod cancel; +mod deploy_dca; +mod deploy_solver; +mod get_order; + +use crate::error::ApiError; +use alloy::primitives::B256; +use async_trait::async_trait; +use rain_orderbook_common::raindex_client::order_quotes::RaindexOrderQuote; +use rain_orderbook_common::raindex_client::orders::{GetOrdersFilters, RaindexOrder}; +use rain_orderbook_common::raindex_client::trades::RaindexTrade; +use rain_orderbook_common::raindex_client::RaindexClient; +use rocket::Route; + +#[async_trait(?Send)] +pub(crate) trait OrderDataSource { + async fn get_orders_by_hash(&self, hash: B256) -> Result, ApiError>; + async fn get_order_quotes(&self, order: &RaindexOrder) -> Vec; + async fn get_order_trades(&self, order: &RaindexOrder) -> Vec; +} + +pub(crate) struct RaindexOrderDataSource<'a> { + pub client: &'a RaindexClient, +} + +#[async_trait(?Send)] +impl<'a> OrderDataSource for RaindexOrderDataSource<'a> { + async fn get_orders_by_hash(&self, hash: B256) -> Result, ApiError> { + let filters = GetOrdersFilters { + order_hash: Some(hash), + ..Default::default() + }; + self.client + .get_orders(None, Some(filters), None) + .await + .map_err(|e| { + tracing::error!(error = %e, "failed to query orders"); + ApiError::Internal("failed to query orders".into()) + }) + } + + async fn get_order_quotes(&self, order: &RaindexOrder) -> Vec { + match order.get_quotes(None, None).await { + Ok(quotes) => quotes, + Err(e) => { + tracing::warn!(error = %e, "failed to fetch order quotes"); + vec![] + } + } + } + + async fn get_order_trades(&self, order: &RaindexOrder) -> Vec { + match order.get_trades_list(None, None, None).await { + Ok(t) => t, + Err(e) => { + tracing::warn!(error = %e, "failed to fetch order trades"); + vec![] + } + } + } +} + +pub use cancel::*; +pub use deploy_dca::*; +pub use deploy_solver::*; +pub use get_order::*; + +pub fn routes() -> Vec { + rocket::routes![ + deploy_dca::post_order_dca, + deploy_solver::post_order_solver, + get_order::get_order, + cancel::post_order_cancel + ] +} From 0fd4797038569ca335751937c2442ee5ec21c3eb Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 16 Feb 2026 12:22:12 +0100 Subject: [PATCH 04/10] order: propagate errors from OrderDataSource quotes and trades methods --- src/routes/order/get_order.rs | 18 ++++++++++----- src/routes/order/mod.rs | 42 ++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/routes/order/get_order.rs b/src/routes/order/get_order.rs index 9238bb7..56c3bda 100644 --- a/src/routes/order/get_order.rs +++ b/src/routes/order/get_order.rs @@ -20,13 +20,13 @@ async fn process_get_order(ds: &dyn OrderDataSource, hash: B256) -> Result Err(ApiError::Internal("failed to query orders".into())), } } - async fn get_order_quotes(&self, _order: &RaindexOrder) -> Vec { - self.quotes.clone() + async fn get_order_quotes( + &self, + _order: &RaindexOrder, + ) -> Result, ApiError> { + Ok(self.quotes.clone()) } - async fn get_order_trades(&self, _order: &RaindexOrder) -> Vec { - self.trades.clone() + async fn get_order_trades( + &self, + _order: &RaindexOrder, + ) -> Result, ApiError> { + Ok(self.trades.clone()) } } diff --git a/src/routes/order/mod.rs b/src/routes/order/mod.rs index 3439b6b..19eb9dd 100644 --- a/src/routes/order/mod.rs +++ b/src/routes/order/mod.rs @@ -15,8 +15,14 @@ use rocket::Route; #[async_trait(?Send)] pub(crate) trait OrderDataSource { async fn get_orders_by_hash(&self, hash: B256) -> Result, ApiError>; - async fn get_order_quotes(&self, order: &RaindexOrder) -> Vec; - async fn get_order_trades(&self, order: &RaindexOrder) -> Vec; + async fn get_order_quotes( + &self, + order: &RaindexOrder, + ) -> Result, ApiError>; + async fn get_order_trades( + &self, + order: &RaindexOrder, + ) -> Result, ApiError>; } pub(crate) struct RaindexOrderDataSource<'a> { @@ -39,24 +45,24 @@ impl<'a> OrderDataSource for RaindexOrderDataSource<'a> { }) } - async fn get_order_quotes(&self, order: &RaindexOrder) -> Vec { - match order.get_quotes(None, None).await { - Ok(quotes) => quotes, - Err(e) => { - tracing::warn!(error = %e, "failed to fetch order quotes"); - vec![] - } - } + async fn get_order_quotes( + &self, + order: &RaindexOrder, + ) -> Result, ApiError> { + order.get_quotes(None, None).await.map_err(|e| { + tracing::error!(error = %e, "failed to query order quotes"); + ApiError::Internal("failed to query order quotes".into()) + }) } - async fn get_order_trades(&self, order: &RaindexOrder) -> Vec { - match order.get_trades_list(None, None, None).await { - Ok(t) => t, - Err(e) => { - tracing::warn!(error = %e, "failed to fetch order trades"); - vec![] - } - } + async fn get_order_trades( + &self, + order: &RaindexOrder, + ) -> Result, ApiError> { + order.get_trades_list(None, None, None).await.map_err(|e| { + tracing::error!(error = %e, "failed to query order trades"); + ApiError::Internal("failed to query order trades".into()) + }) } } From 91605fbde131e23b895b0e78e06b288ac3c46d89 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 16 Feb 2026 12:26:45 +0100 Subject: [PATCH 05/10] order: move process_get_order below route handler for readability --- src/routes/order/get_order.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/routes/order/get_order.rs b/src/routes/order/get_order.rs index 56c3bda..7bacb25 100644 --- a/src/routes/order/get_order.rs +++ b/src/routes/order/get_order.rs @@ -14,23 +14,6 @@ use rocket::serde::json::Json; use rocket::State; use tracing::Instrument; -async fn process_get_order(ds: &dyn OrderDataSource, hash: B256) -> Result { - let orders = ds.get_orders_by_hash(hash).await?; - let order = orders - .into_iter() - .next() - .ok_or_else(|| ApiError::NotFound("order not found".into()))?; - let quotes = ds.get_order_quotes(&order).await.unwrap_or_default(); - let io_ratio = quotes - .first() - .and_then(|q| q.data.as_ref()) - .map(|d| d.formatted_ratio.clone()) - .unwrap_or_else(|| "-".into()); - let trades = ds.get_order_trades(&order).await.unwrap_or_default(); - let order_type = determine_order_type(&order); - build_order_detail(&order, order_type, &io_ratio, &trades) -} - #[utoipa::path( get, path = "/v1/order/{order_hash}", @@ -71,6 +54,23 @@ pub async fn get_order( .await } +async fn process_get_order(ds: &dyn OrderDataSource, hash: B256) -> Result { + let orders = ds.get_orders_by_hash(hash).await?; + let order = orders + .into_iter() + .next() + .ok_or_else(|| ApiError::NotFound("order not found".into()))?; + let quotes = ds.get_order_quotes(&order).await.unwrap_or_default(); + let io_ratio = quotes + .first() + .and_then(|q| q.data.as_ref()) + .map(|d| d.formatted_ratio.clone()) + .unwrap_or_else(|| "-".into()); + let trades = ds.get_order_trades(&order).await.unwrap_or_default(); + let order_type = determine_order_type(&order); + build_order_detail(&order, order_type, &io_ratio, &trades) +} + fn determine_order_type(order: &RaindexOrder) -> OrderType { for meta in order.parsed_meta() { if let ParsedMeta::DotrainGuiStateV1(gui_state) = meta { From cf5cea42cd58499069ff36c5348ccd3ccc65f6c1 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 16 Feb 2026 12:27:58 +0100 Subject: [PATCH 06/10] chore: apply formatting fixes and add async-trait dependency --- Cargo.lock | 1 + src/routes/order/get_order.rs | 15 +++------------ src/routes/order/mod.rs | 10 ++-------- src/test_helpers.rs | 2 +- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db996b5..e17876d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8870,6 +8870,7 @@ version = "0.1.0" dependencies = [ "alloy", "argon2", + "async-trait", "base64 0.22.1", "clap", "rain_orderbook_common", diff --git a/src/routes/order/get_order.rs b/src/routes/order/get_order.rs index 7bacb25..3106b75 100644 --- a/src/routes/order/get_order.rs +++ b/src/routes/order/get_order.rs @@ -3,9 +3,7 @@ use crate::auth::AuthenticatedKey; use crate::error::{ApiError, ApiErrorResponse}; use crate::fairings::{GlobalRateLimit, TracingSpan}; use crate::types::common::{TokenRef, ValidatedFixedBytes}; -use crate::types::order::{ - OrderDetail, OrderDetailsInfo, OrderTradeEntry, OrderType, -}; +use crate::types::order::{OrderDetail, OrderDetailsInfo, OrderTradeEntry, OrderType}; use alloy::primitives::B256; use rain_orderbook_common::parsed_meta::ParsedMeta; use rain_orderbook_common::raindex_client::orders::RaindexOrder; @@ -74,11 +72,7 @@ async fn process_get_order(ds: &dyn OrderDataSource, hash: B256) -> Result OrderType { for meta in order.parsed_meta() { if let ParsedMeta::DotrainGuiStateV1(gui_state) = meta { - if gui_state - .selected_deployment - .to_lowercase() - .contains("dca") - { + if gui_state.selected_deployment.to_lowercase().contains("dca") { return OrderType::Dca; } } @@ -109,10 +103,7 @@ fn build_order_detail( let trade_entries: Vec = trades.iter().map(map_trade).collect(); - let created_at: u64 = order - .timestamp_added() - .try_into() - .unwrap_or(0); + let created_at: u64 = order.timestamp_added().try_into().unwrap_or(0); Ok(OrderDetail { order_hash: order.order_hash(), diff --git a/src/routes/order/mod.rs b/src/routes/order/mod.rs index 19eb9dd..dc580ea 100644 --- a/src/routes/order/mod.rs +++ b/src/routes/order/mod.rs @@ -19,10 +19,7 @@ pub(crate) trait OrderDataSource { &self, order: &RaindexOrder, ) -> Result, ApiError>; - async fn get_order_trades( - &self, - order: &RaindexOrder, - ) -> Result, ApiError>; + async fn get_order_trades(&self, order: &RaindexOrder) -> Result, ApiError>; } pub(crate) struct RaindexOrderDataSource<'a> { @@ -55,10 +52,7 @@ impl<'a> OrderDataSource for RaindexOrderDataSource<'a> { }) } - async fn get_order_trades( - &self, - order: &RaindexOrder, - ) -> Result, ApiError> { + async fn get_order_trades(&self, order: &RaindexOrder) -> Result, ApiError> { order.get_trades_list(None, None, None).await.map_err(|e| { tracing::error!(error = %e, "failed to query order trades"); ApiError::Internal("failed to query order trades".into()) diff --git a/src/test_helpers.rs b/src/test_helpers.rs index d84c6bb..820797e 100644 --- a/src/test_helpers.rs +++ b/src/test_helpers.rs @@ -37,7 +37,7 @@ impl TestClientBuilder { self } -pub(crate) async fn build(self) -> Client { + pub(crate) async fn build(self) -> Client { let id = uuid::Uuid::new_v4(); let pool = crate::db::init(&format!("sqlite:file:{id}?mode=memory&cache=shared")) .await From 1c62832f314efda2eae9ef39c5cb34f645aa7ba1 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 23 Feb 2026 13:55:04 +0300 Subject: [PATCH 07/10] order: propagate errors from quotes and trades lookups Replace .unwrap_or_default() with ? on get_order_quotes and get_order_trades so failures surface as ApiError instead of being silently swallowed. Update MockOrderDataSource to wrap quotes and trades in Result and add tests for both error paths. --- src/routes/order/get_order.rs | 60 +++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/src/routes/order/get_order.rs b/src/routes/order/get_order.rs index b06de80..5ec56f5 100644 --- a/src/routes/order/get_order.rs +++ b/src/routes/order/get_order.rs @@ -59,13 +59,13 @@ async fn process_get_order(ds: &dyn OrderDataSource, hash: B256) -> Result, ApiError>, - trades: Vec, - quotes: Vec, + trades: Result, ApiError>, + quotes: Result, ApiError>, } #[async_trait(?Send)] @@ -365,13 +365,19 @@ mod tests { &self, _order: &RaindexOrder, ) -> Result, ApiError> { - Ok(self.quotes.clone()) + match &self.quotes { + Ok(quotes) => Ok(quotes.clone()), + Err(_) => Err(ApiError::Internal("failed to query order quotes".into())), + } } async fn get_order_trades( &self, _order: &RaindexOrder, ) -> Result, ApiError> { - Ok(self.trades.clone()) + match &self.trades { + Ok(trades) => Ok(trades.clone()), + Err(_) => Err(ApiError::Internal("failed to query order trades".into())), + } } } @@ -385,8 +391,8 @@ mod tests { async fn test_process_get_order_success() { let ds = MockOrderDataSource { orders: Ok(vec![mock_order()]), - trades: vec![mock_trade()], - quotes: vec![mock_quote("1.5")], + trades: Ok(vec![mock_trade()]), + quotes: Ok(vec![mock_quote("1.5")]), }; let detail = process_get_order(&ds, test_hash()).await.unwrap(); @@ -415,8 +421,8 @@ mod tests { async fn test_process_get_order_not_found() { let ds = MockOrderDataSource { orders: Ok(vec![]), - trades: vec![], - quotes: vec![], + trades: Ok(vec![]), + quotes: Ok(vec![]), }; let result = process_get_order(&ds, test_hash()).await; assert!(matches!(result, Err(ApiError::NotFound(_)))); @@ -426,8 +432,8 @@ mod tests { async fn test_process_get_order_empty_trades() { let ds = MockOrderDataSource { orders: Ok(vec![mock_order()]), - trades: vec![], - quotes: vec![mock_quote("2.0")], + trades: Ok(vec![]), + quotes: Ok(vec![mock_quote("2.0")]), }; let detail = process_get_order(&ds, test_hash()).await.unwrap(); assert!(detail.trades.is_empty()); @@ -438,8 +444,8 @@ mod tests { async fn test_process_get_order_failed_quote() { let ds = MockOrderDataSource { orders: Ok(vec![mock_order()]), - trades: vec![], - quotes: vec![mock_failed_quote()], + trades: Ok(vec![]), + quotes: Ok(vec![mock_failed_quote()]), }; let detail = process_get_order(&ds, test_hash()).await.unwrap(); assert_eq!(detail.io_ratio, "-"); @@ -450,8 +456,30 @@ mod tests { async fn test_process_get_order_query_failure() { let ds = MockOrderDataSource { orders: Err(ApiError::Internal("failed to query orders".into())), - trades: vec![], - quotes: vec![], + trades: Ok(vec![]), + quotes: Ok(vec![]), + }; + let result = process_get_order(&ds, test_hash()).await; + assert!(matches!(result, Err(ApiError::Internal(_)))); + } + + #[rocket::async_test] + async fn test_process_get_order_quotes_failure() { + let ds = MockOrderDataSource { + orders: Ok(vec![mock_order()]), + trades: Ok(vec![]), + quotes: Err(ApiError::Internal("failed to query order quotes".into())), + }; + let result = process_get_order(&ds, test_hash()).await; + assert!(matches!(result, Err(ApiError::Internal(_)))); + } + + #[rocket::async_test] + async fn test_process_get_order_trades_failure() { + let ds = MockOrderDataSource { + orders: Ok(vec![mock_order()]), + trades: Err(ApiError::Internal("failed to query order trades".into())), + quotes: Ok(vec![mock_quote("1.5")]), }; let result = process_get_order(&ds, test_hash()).await; assert!(matches!(result, Err(ApiError::Internal(_)))); From f2452cc176dedc2166c7aa834f694f314e8e9514 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 23 Feb 2026 14:00:21 +0300 Subject: [PATCH 08/10] chore: update Cargo.lock --- Cargo.lock | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4044fad..8d5ed82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1579,13 +1579,12 @@ dependencies = [ [[package]] name = "backon" -version = "0.4.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d67782c3f868daa71d3533538e98a8e13713231969def7536e8039606fc46bf0" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ "fastrand", - "futures-core", - "pin-project", + "gloo-timers 0.3.0", "tokio", ] @@ -3975,6 +3974,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "graphql-introspection-query" version = "0.2.0" @@ -6610,7 +6621,7 @@ dependencies = [ "flate2", "futures", "getrandom 0.2.16", - "gloo-timers", + "gloo-timers 0.2.6", "itertools 0.14.0", "once_cell", "proptest", From daec49eae8d5ff65dc2a54253bbd55bf52e30ee1 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 23 Feb 2026 14:04:40 +0300 Subject: [PATCH 09/10] order: document single-pair vault assumption --- src/routes/order/get_order.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/order/get_order.rs b/src/routes/order/get_order.rs index 5ec56f5..43a1f50 100644 --- a/src/routes/order/get_order.rs +++ b/src/routes/order/get_order.rs @@ -87,6 +87,7 @@ fn build_order_detail( io_ratio: &str, trades: &[RaindexTrade], ) -> Result { + // The current application only supports single-pair orders (one input vault, one output vault). let inputs = order.inputs_list().items(); let outputs = order.outputs_list().items(); From 947c70a5bdbd4f616cd3c97f66357b5659a5d6f7 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 23 Feb 2026 15:03:53 +0300 Subject: [PATCH 10/10] fix: remove duplicate async-trait dependency from merge --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6a759f2..a45f199 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ rain_orderbook_js_api = { path = "lib/rain.orderbook/crates/js_api", default-fea 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]