diff --git a/src/routes/trades/get_by_tx.rs b/src/routes/trades/get_by_tx.rs index 7fceee6..a69a654 100644 --- a/src/routes/trades/get_by_tx.rs +++ b/src/routes/trades/get_by_tx.rs @@ -1,4 +1,4 @@ -use super::{RaindexTradesDataSource, TradesDataSource}; +use super::{RaindexTradesDataSource, TradesDataSource, TxIndexState}; use crate::auth::AuthenticatedKey; use crate::error::{ApiError, ApiErrorResponse}; use crate::fairings::{GlobalRateLimit, TracingSpan}; @@ -56,9 +56,14 @@ pub(super) async fn process_get_trades_by_tx( let trades = result.trades(); if trades.is_empty() { - return Err(ApiError::NotFound( - "transaction has no associated trades".into(), - )); + return match trades_ds.check_tx_index_state(tx_hash).await? { + TxIndexState::NotYetIndexed => Err(ApiError::NotYetIndexed(format!( + "transaction {tx_hash:#x} not yet indexed" + ))), + TxIndexState::Indexed => Err(ApiError::NotFound( + "transaction has no associated trades".into(), + )), + }; } let first_tx = trades[0].transaction(); @@ -130,6 +135,7 @@ mod tests { struct MockTradesDataSource { result: Result, + tx_index_state: Result, } #[async_trait] @@ -143,12 +149,17 @@ mod tests { Err(e) => Err(e.clone()), } } + + async fn check_tx_index_state(&self, _tx_hash: B256) -> Result { + self.tx_index_state.clone() + } } #[rocket::async_test] async fn test_process_success() { let trades_ds = MockTradesDataSource { result: Ok(mock_trades_list_result()), + tx_index_state: Ok(TxIndexState::Indexed), }; let result = process_get_trades_by_tx( &trades_ds, @@ -177,6 +188,7 @@ mod tests { async fn test_process_tx_not_found() { let trades_ds = MockTradesDataSource { result: Ok(mock_empty_trades_list_result()), + tx_index_state: Ok(TxIndexState::Indexed), }; let result = process_get_trades_by_tx( &trades_ds, @@ -191,7 +203,8 @@ mod tests { #[rocket::async_test] async fn test_process_tx_not_indexed() { let trades_ds = MockTradesDataSource { - result: Err(ApiError::NotYetIndexed("not indexed".into())), + result: Ok(mock_empty_trades_list_result()), + tx_index_state: Ok(TxIndexState::NotYetIndexed), }; let result = process_get_trades_by_tx( &trades_ds, @@ -207,6 +220,23 @@ mod tests { async fn test_process_query_failure() { let trades_ds = MockTradesDataSource { result: Err(ApiError::Internal("subgraph error".into())), + tx_index_state: Ok(TxIndexState::Indexed), + }; + let result = process_get_trades_by_tx( + &trades_ds, + "0x0000000000000000000000000000000000000000000000000000000000000001" + .parse() + .unwrap(), + ) + .await; + assert!(matches!(result, Err(ApiError::Internal(_)))); + } + + #[rocket::async_test] + async fn test_process_index_check_failure() { + let trades_ds = MockTradesDataSource { + result: Ok(mock_empty_trades_list_result()), + tx_index_state: Err(ApiError::Internal("index check failed".into())), }; let result = process_get_trades_by_tx( &trades_ds, diff --git a/src/routes/trades/mod.rs b/src/routes/trades/mod.rs index 515a497..4f5a0b1 100644 --- a/src/routes/trades/mod.rs +++ b/src/routes/trades/mod.rs @@ -8,9 +8,17 @@ use rain_orderbook_common::raindex_client::trades::RaindexTradesListResult; use rain_orderbook_common::raindex_client::{RaindexClient, RaindexError}; use rocket::Route; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TxIndexState { + Indexed, + NotYetIndexed, +} + #[async_trait] pub(crate) trait TradesDataSource: Send + Sync { async fn get_trades_by_tx(&self, tx_hash: B256) -> Result; + + async fn check_tx_index_state(&self, tx_hash: B256) -> Result; } pub(crate) struct RaindexTradesDataSource<'a> { @@ -40,6 +48,50 @@ impl TradesDataSource for RaindexTradesDataSource<'_> { } }) } + + async fn check_tx_index_state(&self, tx_hash: B256) -> Result { + let orderbooks = self.client.get_all_orderbooks().map_err(|e| { + tracing::error!(error = %e, "failed to get orderbooks"); + ApiError::Internal("failed to query transaction".into()) + })?; + + let mut saw_timeout = false; + + for orderbook in orderbooks.values() { + match self + .client + .get_transaction( + orderbook.network.chain_id, + orderbook.address, + tx_hash, + Some(1), + Some(0), + ) + .await + { + Ok(_) => return Ok(TxIndexState::Indexed), + Err(RaindexError::TransactionIndexingTimeout { .. }) => { + saw_timeout = true; + } + Err(err) => { + tracing::error!( + error = %err, + tx_hash = %tx_hash, + chain_id = orderbook.network.chain_id, + orderbook = %orderbook.address, + "failed to query transaction status" + ); + return Err(ApiError::Internal("failed to query transaction".into())); + } + } + } + + if saw_timeout { + return Ok(TxIndexState::NotYetIndexed); + } + + Ok(TxIndexState::Indexed) + } } pub fn routes() -> Vec {