From 9bc185988b609fe0848b61815abed94aff3ea913 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 12:19:35 +0300 Subject: [PATCH 01/12] feat: add owner to SgTradeStructPartialOrder and new trade query types Add owner field to SgTradeStructPartialOrder for trade-level owner tracking. Add SgPaginationWithTxIdQueryVariables and SgTransactionTradesQuery for querying trades by transaction hash. Add SgTradeWithSubgraphName wrapper type for multi-subgraph results. --- .../src/orderbook_client/performance.rs | 1 + crates/subgraph/src/performance/apy.rs | 2 ++ .../src/performance/order_performance.rs | 2 ++ crates/subgraph/src/performance/vol.rs | 2 ++ crates/subgraph/src/types/common.rs | 19 ++++++++++++++++++ crates/subgraph/src/types/impls.rs | 1 + crates/subgraph/src/types/order_trade.rs | 20 +++++++++++++++++++ 7 files changed, 47 insertions(+) diff --git a/crates/subgraph/src/orderbook_client/performance.rs b/crates/subgraph/src/orderbook_client/performance.rs index 45b185427b..904a4a554c 100644 --- a/crates/subgraph/src/orderbook_client/performance.rs +++ b/crates/subgraph/src/orderbook_client/performance.rs @@ -74,6 +74,7 @@ mod tests { order: SgTradeStructPartialOrder { id: SgBytes(order_id_str.to_string()), order_hash: SgBytes(format!("0xhash_{}", order_id_str)), + owner: SgBytes("0xowner_default".to_string()), }, orderbook: SgOrderbook { id: SgBytes("0xorderbook_default".to_string()), diff --git a/crates/subgraph/src/performance/apy.rs b/crates/subgraph/src/performance/apy.rs index a30a3a35f6..3f4bb1ebbc 100644 --- a/crates/subgraph/src/performance/apy.rs +++ b/crates/subgraph/src/performance/apy.rs @@ -347,6 +347,7 @@ mod tests { order: SgTradeStructPartialOrder { id: bytes.clone(), order_hash: bytes.clone(), + owner: bytes.clone(), }, trade_event: SgTradeEvent { sender: bytes.clone(), @@ -406,6 +407,7 @@ mod tests { order: SgTradeStructPartialOrder { id: bytes.clone(), order_hash: bytes.clone(), + owner: bytes.clone(), }, trade_event: SgTradeEvent { sender: bytes.clone(), diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index 140c031cd6..f9abeb933f 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -849,6 +849,7 @@ mod test { order: SgTradeStructPartialOrder { id: bytes.clone(), order_hash: bytes.clone(), + owner: bytes.clone(), }, trade_event: SgTradeEvent { sender: bytes.clone(), @@ -908,6 +909,7 @@ mod test { order: SgTradeStructPartialOrder { id: bytes.clone(), order_hash: bytes.clone(), + owner: bytes.clone(), }, trade_event: SgTradeEvent { sender: bytes.clone(), diff --git a/crates/subgraph/src/performance/vol.rs b/crates/subgraph/src/performance/vol.rs index 30b0b92602..99ed62c0bc 100644 --- a/crates/subgraph/src/performance/vol.rs +++ b/crates/subgraph/src/performance/vol.rs @@ -232,6 +232,7 @@ mod tests { order: SgTradeStructPartialOrder { id: bytes.clone(), order_hash: bytes.clone(), + owner: bytes.clone(), }, trade_event: SgTradeEvent { sender: bytes.clone(), @@ -301,6 +302,7 @@ mod tests { order: SgTradeStructPartialOrder { id: bytes.clone(), order_hash: bytes.clone(), + owner: bytes.clone(), }, trade_event: SgTradeEvent { sender: bytes.clone(), diff --git a/crates/subgraph/src/types/common.rs b/crates/subgraph/src/types/common.rs index d3d6e72e1b..faa4c44298 100644 --- a/crates/subgraph/src/types/common.rs +++ b/crates/subgraph/src/types/common.rs @@ -102,6 +102,17 @@ pub struct SgPaginationWithTimestampQueryVariables { pub timestamp_lte: Option, } +#[derive(cynic::QueryVariables, Debug, Clone, Tsify)] +pub struct SgPaginationWithTxIdQueryVariables { + #[cfg_attr(target_family = "wasm", tsify(optional))] + pub first: Option, + #[cfg_attr(target_family = "wasm", tsify(optional))] + pub skip: Option, + pub tx_id: String, + #[cfg_attr(target_family = "wasm", tsify(optional))] + pub orderbook_in: Option>, +} + #[derive(cynic::QueryFragment, Debug, Serialize, Clone, Tsify)] #[cynic(graphql_type = "Orderbook")] pub struct SgOrderbook { @@ -140,12 +151,20 @@ pub struct SgOrderWithSubgraphName { } impl_wasm_traits!(SgOrderWithSubgraphName); +#[derive(Debug, Serialize, Deserialize, Clone, Tsify)] +#[serde(rename_all = "camelCase")] +pub struct SgTradeWithSubgraphName { + pub trade: SgTrade, + pub subgraph_name: String, +} + #[derive(cynic::QueryFragment, Debug, Serialize, Clone, Tsify)] #[cynic(graphql_type = "Order")] #[serde(rename_all = "camelCase")] pub struct SgTradeStructPartialOrder { pub id: SgBytes, pub order_hash: SgBytes, + pub owner: SgBytes, } #[derive(cynic::QueryFragment, Debug, Serialize, Clone, Tsify)] diff --git a/crates/subgraph/src/types/impls.rs b/crates/subgraph/src/types/impls.rs index 5013ce50c9..b80eafa1bb 100644 --- a/crates/subgraph/src/types/impls.rs +++ b/crates/subgraph/src/types/impls.rs @@ -239,6 +239,7 @@ mod tests { order: SgTradeStructPartialOrder { id: SgBytes("".to_string()), order_hash: SgBytes("".to_string()), + owner: SgBytes("".to_string()), }, timestamp: SgBigInt("".to_string()), orderbook: SgOrderbook { diff --git a/crates/subgraph/src/types/order_trade.rs b/crates/subgraph/src/types/order_trade.rs index bb29d45e97..844a0e41dd 100644 --- a/crates/subgraph/src/types/order_trade.rs +++ b/crates/subgraph/src/types/order_trade.rs @@ -33,3 +33,23 @@ pub struct SgOrderTradeDetailQuery { #[cfg_attr(target_family = "wasm", tsify(optional))] pub trade: Option, } + +#[derive(cynic::QueryFragment, Debug, Clone, Serialize)] +#[cynic( + graphql_type = "Query", + variables = "SgPaginationWithTxIdQueryVariables" +)] +#[cfg_attr(target_family = "wasm", derive(Tsify))] +pub struct SgTransactionTradesQuery { + #[arguments( + skip: $skip, + first: $first, + orderBy: "timestamp", + orderDirection: "desc", + where: { + tradeEvent_: { transaction: $tx_id }, + orderbook_in: $orderbook_in + } + )] + pub trades: Vec, +} From 4c234a0155c880399d718a167a4ebd380dc038af Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 12:19:40 +0300 Subject: [PATCH 02/12] feat: add trades_by_transaction to subgraph clients Add paginated trades_by_transaction method to OrderbookSubgraphClient for querying trades by transaction hash with optional orderbook filter. Add corresponding fan-out method to MultiOrderbookSubgraphClient that queries all configured subgraphs in parallel. --- crates/subgraph/src/multi_orderbook_client.rs | 308 +++++++++++++++++- crates/subgraph/src/orderbook_client/mod.rs | 4 +- .../src/orderbook_client/order_trade.rs | 159 +++++++++ 3 files changed, 468 insertions(+), 3 deletions(-) diff --git a/crates/subgraph/src/multi_orderbook_client.rs b/crates/subgraph/src/multi_orderbook_client.rs index c51afdacc0..8c1b8ab33f 100644 --- a/crates/subgraph/src/multi_orderbook_client.rs +++ b/crates/subgraph/src/multi_orderbook_client.rs @@ -1,7 +1,7 @@ use crate::{ types::common::{ SgErc20WithSubgraphName, SgOrderWithSubgraphName, SgOrdersListFilterArgs, - SgVaultWithSubgraphName, SgVaultsListFilterArgs, + SgTradeWithSubgraphName, SgVaultWithSubgraphName, SgVaultsListFilterArgs, }, OrderbookSubgraphClient, OrderbookSubgraphClientError, SgPaginationArgs, }; @@ -141,6 +141,40 @@ impl MultiOrderbookSubgraphClient { Ok(all_vaults) } + pub async fn trades_by_transaction( + &self, + tx_id: String, + orderbook_in: Option>, + ) -> Vec { + let futures = self.subgraphs.iter().map(|subgraph| { + let url = subgraph.url.clone(); + let tx_id = tx_id.clone(); + let orderbook_in = orderbook_in.clone(); + async move { + let client = self.get_orderbook_subgraph_client(url); + let trades = client.trades_by_transaction(tx_id, orderbook_in).await?; + let wrapped_trades: Vec = trades + .into_iter() + .map(|trade| SgTradeWithSubgraphName { + trade, + subgraph_name: subgraph.name.clone(), + }) + .collect(); + Ok::<_, OrderbookSubgraphClientError>(wrapped_trades) + } + }); + + let results = join_all(futures).await; + + let all_trades: Vec = results + .into_iter() + .filter_map(Result::ok) + .flatten() + .collect(); + + all_trades + } + pub async fn tokens_list( &self, ) -> Result, OrderbookSubgraphClientError> { @@ -185,7 +219,9 @@ mod tests { use super::*; use crate::cynic_client::CynicClientError; use crate::types::common::{ - SgBigInt, SgBytes, SgErc20, SgOrder, SgOrderbook, SgOrdersListFilterArgs, SgVault, + SgBigInt, SgBytes, SgErc20, SgOrder, SgOrderbook, SgOrdersListFilterArgs, SgTrade, + SgTradeEvent, SgTradeEventTypename, SgTradeRef, SgTradeStructPartialOrder, + SgTradeVaultBalanceChange, SgTransaction, SgVault, SgVaultBalanceChangeVault, }; use crate::utils::float::*; use httpmock::prelude::*; @@ -757,6 +793,274 @@ mod tests { assert_eq!(count, ALL_PAGES_QUERY_PAGE_SIZE as u32 + 10); } + fn default_sg_transaction() -> SgTransaction { + SgTransaction { + id: SgBytes("0xtransaction_id_default".to_string()), + from: SgBytes("0xfrom_address_default".to_string()), + block_number: SgBigInt("100".to_string()), + timestamp: SgBigInt("1600000000".to_string()), + } + } + + fn default_sg_trade_erc20() -> SgErc20 { + SgErc20 { + id: SgBytes("0xtoken_id_default".to_string()), + address: SgBytes("0xtoken_address_default".to_string()), + name: Some("Default Token".to_string()), + symbol: Some("DTK".to_string()), + decimals: Some(SgBigInt("18".to_string())), + } + } + + fn default_sg_vault_balance_change_vault() -> SgVaultBalanceChangeVault { + SgVaultBalanceChangeVault { + id: SgBytes("0xvault_id_default".to_string()), + vault_id: SgBytes("12345".to_string()), + token: default_sg_trade_erc20(), + } + } + + fn default_sg_trade_event_typename() -> SgTradeEventTypename { + SgTradeEventTypename { + __typename: "TakeOrder".to_string(), + } + } + + fn default_sg_trade_ref() -> SgTradeRef { + SgTradeRef { + trade_event: default_sg_trade_event_typename(), + } + } + + fn default_sg_trade_vault_balance_change(type_name: &str) -> SgTradeVaultBalanceChange { + SgTradeVaultBalanceChange { + id: SgBytes(format!("0xtrade_vbc_{}_id_default", type_name)), + __typename: "TradeVaultBalanceChange".to_string(), + amount: SgBytes(F1.as_hex()), + new_vault_balance: SgBytes(F5.as_hex()), + old_vault_balance: SgBytes(F4.as_hex()), + vault: default_sg_vault_balance_change_vault(), + timestamp: SgBigInt("1600000100".to_string()), + transaction: default_sg_transaction(), + orderbook: SgOrderbook { + id: SgBytes("0xorderbook_id_default".to_string()), + }, + trade: default_sg_trade_ref(), + } + } + + fn default_sg_trade_event() -> SgTradeEvent { + SgTradeEvent { + transaction: default_sg_transaction(), + sender: SgBytes("0xsender_address_default".to_string()), + } + } + + fn default_sg_trade_struct_partial_order() -> SgTradeStructPartialOrder { + SgTradeStructPartialOrder { + id: SgBytes("0xorder_id_for_trade_default".to_string()), + order_hash: SgBytes("0xorder_hash_for_trade_default".to_string()), + owner: SgBytes("0xowner_address_default".to_string()), + } + } + + fn default_sg_trade() -> SgTrade { + SgTrade { + id: SgBytes("0xtrade_id_default".to_string()), + trade_event: default_sg_trade_event(), + output_vault_balance_change: default_sg_trade_vault_balance_change("output"), + order: default_sg_trade_struct_partial_order(), + input_vault_balance_change: default_sg_trade_vault_balance_change("input"), + timestamp: SgBigInt("1600000200".to_string()), + orderbook: SgOrderbook { + id: SgBytes("0xorderbook_id_default".to_string()), + }, + } + } + + #[tokio::test] + async fn test_trades_by_transaction_no_subgraphs() { + let client = MultiOrderbookSubgraphClient::new(vec![]); + let result = client.trades_by_transaction("0xtx123".to_string(), None).await; + assert!(result.is_empty()); + } + + #[tokio::test] + async fn test_trades_by_transaction_one_subgraph_returns_trades() { + use crate::orderbook_client::ALL_PAGES_QUERY_PAGE_SIZE; + + let server1 = MockServer::start_async().await; + let sg1_url = Url::parse(&server1.url("")).unwrap(); + let sg1_name = "subgraph_alpha"; + + let trade1 = default_sg_trade(); + server1.mock(|when, then| { + when.method(POST).path("/").body_contains("\"skip\":0"); + then.status(200) + .json_body(json!({"data": {"trades": [trade1]}})); + }); + server1.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); + then.status(200).json_body(json!({"data": {"trades": []}})); + }); + + let client = MultiOrderbookSubgraphClient::new(vec![MultiSubgraphArgs { + url: sg1_url, + name: sg1_name.to_string(), + }]); + + let trades = client + .trades_by_transaction("0xtx_abc".to_string(), None) + .await; + assert_eq!(trades.len(), 1); + assert_eq!(trades[0].trade.id, trade1.id); + assert_eq!(trades[0].subgraph_name, sg1_name); + } + + #[tokio::test] + async fn test_trades_by_transaction_multiple_subgraphs_merge() { + use crate::orderbook_client::ALL_PAGES_QUERY_PAGE_SIZE; + + let server1 = MockServer::start_async().await; + let sg1_url = Url::parse(&server1.url("")).unwrap(); + let sg1_name = "sg_one"; + + let server2 = MockServer::start_async().await; + let sg2_url = Url::parse(&server2.url("")).unwrap(); + let sg2_name = "sg_two"; + + let trade_s1 = default_sg_trade(); + let trade_s2 = default_sg_trade(); + + server1.mock(|when, then| { + when.method(POST).path("/").body_contains("\"skip\":0"); + then.status(200) + .json_body(json!({"data": {"trades": [trade_s1]}})); + }); + server1.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); + then.status(200).json_body(json!({"data": {"trades": []}})); + }); + server2.mock(|when, then| { + when.method(POST).path("/").body_contains("\"skip\":0"); + then.status(200) + .json_body(json!({"data": {"trades": [trade_s2]}})); + }); + server2.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); + then.status(200).json_body(json!({"data": {"trades": []}})); + }); + + let client = MultiOrderbookSubgraphClient::new(vec![ + MultiSubgraphArgs { + url: sg1_url, + name: sg1_name.to_string(), + }, + MultiSubgraphArgs { + url: sg2_url, + name: sg2_name.to_string(), + }, + ]); + + let trades = client + .trades_by_transaction("0xtx_multi".to_string(), None) + .await; + assert_eq!(trades.len(), 2); + + let names: std::collections::HashSet<_> = + trades.iter().map(|t| t.subgraph_name.clone()).collect(); + assert!(names.contains(sg1_name)); + assert!(names.contains(sg2_name)); + } + + #[tokio::test] + async fn test_trades_by_transaction_one_subgraph_errors_others_succeed() { + use crate::orderbook_client::ALL_PAGES_QUERY_PAGE_SIZE; + + let server1 = MockServer::start_async().await; + let sg1_url = Url::parse(&server1.url("")).unwrap(); + let sg1_name = "sg_one_ok"; + + let server2 = MockServer::start_async().await; + let sg2_url = Url::parse(&server2.url("")).unwrap(); + let sg2_name = "sg_two_error"; + + let trade_s1 = default_sg_trade(); + server1.mock(|when, then| { + when.method(POST).path("/").body_contains("\"skip\":0"); + then.status(200) + .json_body(json!({"data": {"trades": [trade_s1]}})); + }); + server1.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); + then.status(200).json_body(json!({"data": {"trades": []}})); + }); + server2.mock(|when, then| { + when.method(POST).path("/"); + then.status(500); + }); + + let client = MultiOrderbookSubgraphClient::new(vec![ + MultiSubgraphArgs { + url: sg1_url, + name: sg1_name.to_string(), + }, + MultiSubgraphArgs { + url: sg2_url, + name: sg2_name.to_string(), + }, + ]); + let trades = client + .trades_by_transaction("0xtx_partial".to_string(), None) + .await; + assert_eq!(trades.len(), 1); + assert_eq!(trades[0].trade.id, trade_s1.id); + assert_eq!(trades[0].subgraph_name, sg1_name); + } + + #[tokio::test] + async fn test_trades_by_transaction_all_subgraphs_error() { + let server1 = MockServer::start_async().await; + let sg1_url = Url::parse(&server1.url("")).unwrap(); + let sg1_name = "sg_one_err"; + + let server2 = MockServer::start_async().await; + let sg2_url = Url::parse(&server2.url("")).unwrap(); + let sg2_name = "sg_two_err"; + + server1.mock(|when, then| { + when.method(POST).path("/"); + then.status(500); + }); + server2.mock(|when, then| { + when.method(POST).path("/"); + then.status(500); + }); + + let client = MultiOrderbookSubgraphClient::new(vec![ + MultiSubgraphArgs { + url: sg1_url, + name: sg1_name.to_string(), + }, + MultiSubgraphArgs { + url: sg2_url, + name: sg2_name.to_string(), + }, + ]); + let trades = client + .trades_by_transaction("0xtx_all_err".to_string(), None) + .await; + assert!(trades.is_empty()); + } + fn sample_sg_erc20(id_suffix: &str) -> SgErc20 { SgErc20 { id: SgBytes(format!("0xtoken_id_{}", id_suffix)), diff --git a/crates/subgraph/src/orderbook_client/mod.rs b/crates/subgraph/src/orderbook_client/mod.rs index c26a72993c..fd3d2db0a9 100644 --- a/crates/subgraph/src/orderbook_client/mod.rs +++ b/crates/subgraph/src/orderbook_client/mod.rs @@ -6,7 +6,9 @@ use crate::types::order::{ SgBatchOrderDetailQuery, SgBatchOrderDetailQueryVariables, SgOrderDetailByHashQuery, SgOrderDetailByHashQueryVariables, SgOrderDetailByIdQuery, SgOrderIdList, SgOrdersListQuery, }; -use crate::types::order_trade::{SgOrderTradeDetailQuery, SgOrderTradesListQuery}; +use crate::types::order_trade::{ + SgOrderTradeDetailQuery, SgOrderTradesListQuery, SgTransactionTradesQuery, +}; use crate::types::remove_order::{ SgTransactionRemoveOrdersQuery, TransactionRemoveOrdersVariables, }; diff --git a/crates/subgraph/src/orderbook_client/order_trade.rs b/crates/subgraph/src/orderbook_client/order_trade.rs index 41e4f6e5bb..db47d715f1 100644 --- a/crates/subgraph/src/orderbook_client/order_trade.rs +++ b/crates/subgraph/src/orderbook_client/order_trade.rs @@ -44,6 +44,40 @@ impl OrderbookSubgraphClient { Ok(data.trades) } + pub async fn trades_by_transaction( + &self, + tx_id: String, + orderbook_in: Option>, + ) -> Result, OrderbookSubgraphClientError> { + let mut all_trades = vec![]; + let mut page = 1; + + loop { + let pagination_variables = + Self::parse_pagination_args(SgPaginationArgs { + page, + page_size: ALL_PAGES_QUERY_PAGE_SIZE, + }); + let data = self + .query::( + SgPaginationWithTxIdQueryVariables { + tx_id: tx_id.clone(), + first: pagination_variables.first, + skip: pagination_variables.skip, + orderbook_in: orderbook_in.clone(), + }, + ) + .await?; + + if data.trades.is_empty() { + break; + } + all_trades.extend(data.trades); + page += 1; + } + Ok(all_trades) + } + /// Fetch all pages of order_takes_list query pub async fn order_trades_list_all( &self, @@ -166,6 +200,7 @@ mod tests { SgTradeStructPartialOrder { id: SgBytes("0xorder_id_for_trade_default".to_string()), order_hash: SgBytes("0xorder_hash_for_trade_default".to_string()), + owner: SgBytes("0xowner_address_default".to_string()), } } @@ -472,6 +507,130 @@ mod tests { assert!(result.unwrap().is_empty()); } + #[tokio::test] + async fn test_trades_by_transaction_found() { + let sg_server = MockServer::start_async().await; + let client = setup_client(&sg_server); + let tx_id = "0xtx_abc123".to_string(); + let expected_trades = vec![default_sg_trade(), default_sg_trade()]; + + sg_server.mock(|when, then| { + when.method(POST).path("/"); + then.status(200) + .json_body(json!({"data": {"trades": expected_trades}})); + }); + sg_server.mock(|when, then| { + when.method(POST).path("/"); + then.status(200).json_body(json!({"data": {"trades": []}})); + }); + + let result = client.trades_by_transaction(tx_id, None).await; + assert!(result.is_ok()); + let trades = result.unwrap(); + assert_eq!(trades.len(), expected_trades.len()); + for (actual, expected) in trades.iter().zip(expected_trades.iter()) { + assert_sg_trade_eq(actual, expected); + } + } + + #[tokio::test] + async fn test_trades_by_transaction_empty() { + let sg_server = MockServer::start_async().await; + let client = setup_client(&sg_server); + let tx_id = "0xtx_empty".to_string(); + + sg_server.mock(|when, then| { + when.method(POST).path("/"); + then.status(200).json_body(json!({"data": {"trades": []}})); + }); + + let result = client.trades_by_transaction(tx_id, None).await; + assert!(result.is_ok()); + assert!(result.unwrap().is_empty()); + } + + #[tokio::test] + async fn test_trades_by_transaction_with_orderbook_filter() { + let sg_server = MockServer::start_async().await; + let client = setup_client(&sg_server); + let tx_id = "0xtx_orderbook_filtered".to_string(); + let orderbook = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string(); + + sg_server.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(format!("\"orderbook_in\":[\"{}\"]", orderbook)); + then.status(200).json_body(json!({"data": {"trades": []}})); + }); + + let result = client + .trades_by_transaction(tx_id, Some(vec![orderbook])) + .await; + assert!(result.is_ok()); + assert!(result.unwrap().is_empty()); + } + + #[tokio::test] + async fn test_trades_by_transaction_multiple_pages() { + let sg_server = MockServer::start_async().await; + let client = setup_client(&sg_server); + let tx_id = "0xtx_multi_page".to_string(); + let trades_page1: Vec = (0..ALL_PAGES_QUERY_PAGE_SIZE) + .map(|_| default_sg_trade()) + .collect(); + let trades_page2: Vec = (0..50).map(|_| default_sg_trade()).collect(); + + sg_server.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(format!("\"first\":{}", ALL_PAGES_QUERY_PAGE_SIZE)) + .body_contains("\"skip\":0"); + then.status(200) + .json_body(json!({"data": {"trades": trades_page1}})); + }); + sg_server.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(format!("\"first\":{}", ALL_PAGES_QUERY_PAGE_SIZE)) + .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); + then.status(200) + .json_body(json!({"data": {"trades": trades_page2}})); + }); + sg_server.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(format!("\"first\":{}", ALL_PAGES_QUERY_PAGE_SIZE)) + .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE * 2)); + then.status(200).json_body(json!({"data": {"trades": []}})); + }); + + let result = client.trades_by_transaction(tx_id, None).await; + assert!(result.is_ok()); + let trades = result.unwrap(); + assert_eq!( + trades.len(), + ALL_PAGES_QUERY_PAGE_SIZE as usize + trades_page2.len() + ); + } + + #[tokio::test] + async fn test_trades_by_transaction_network_error() { + let sg_server = MockServer::start_async().await; + let client = setup_client(&sg_server); + let tx_id = "0xtx_error".to_string(); + + sg_server.mock(|when, then| { + when.method(POST).path("/"); + then.status(500); + }); + + let result = client.trades_by_transaction(tx_id, None).await; + assert!(matches!( + result, + Err(OrderbookSubgraphClientError::CynicClientError(_)) + )); + } + #[tokio::test] async fn test_order_trades_list_all_network_error_on_page() { let sg_server = MockServer::start_async().await; From c56fc184efce93f0189419b2341958ae3e25535d Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 12:19:47 +0300 Subject: [PATCH 03/12] feat: add chain_id to trade queries and new fetch_trades_by_tx module Add chain_id field to LocalDbOrderTrade and its SQL query for proper chain identification. Add new fetch_trades_by_tx module for querying trades by transaction hash from the local database, supporting filtering by chain IDs and orderbook addresses. --- .../local_db/query/fetch_order_trades/mod.rs | 1 + .../query/fetch_order_trades/query.sql | 1 + .../local_db/query/fetch_trades_by_tx/mod.rs | 128 ++++++ .../query/fetch_trades_by_tx/query.sql | 371 ++++++++++++++++++ crates/common/src/local_db/query/mod.rs | 1 + .../local_db/query/fetch_trades_by_tx.rs | 51 +++ .../src/raindex_client/local_db/query/mod.rs | 1 + .../src/types/order_takes_list_flattened.rs | 1 + 8 files changed, 555 insertions(+) create mode 100644 crates/common/src/local_db/query/fetch_trades_by_tx/mod.rs create mode 100644 crates/common/src/local_db/query/fetch_trades_by_tx/query.sql create mode 100644 crates/common/src/raindex_client/local_db/query/fetch_trades_by_tx.rs diff --git a/crates/common/src/local_db/query/fetch_order_trades/mod.rs b/crates/common/src/local_db/query/fetch_order_trades/mod.rs index f837a23817..803484e260 100644 --- a/crates/common/src/local_db/query/fetch_order_trades/mod.rs +++ b/crates/common/src/local_db/query/fetch_order_trades/mod.rs @@ -9,6 +9,7 @@ const QUERY_TEMPLATE: &str = include_str!("query.sql"); #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct LocalDbOrderTrade { + pub chain_id: u32, pub trade_kind: String, pub orderbook: Address, pub order_hash: B256, diff --git a/crates/common/src/local_db/query/fetch_order_trades/query.sql b/crates/common/src/local_db/query/fetch_order_trades/query.sql index d50b5392f4..8468390adf 100644 --- a/crates/common/src/local_db/query/fetch_order_trades/query.sql +++ b/crates/common/src/local_db/query/fetch_order_trades/query.sql @@ -289,6 +289,7 @@ trade_with_snapshots AS ( AND mvb_out.vault_id = tr.output_vault_id ) SELECT + tws.chain_id, tws.trade_kind, tws.orderbook_address AS orderbook, tws.order_hash, diff --git a/crates/common/src/local_db/query/fetch_trades_by_tx/mod.rs b/crates/common/src/local_db/query/fetch_trades_by_tx/mod.rs new file mode 100644 index 0000000000..7fb56df619 --- /dev/null +++ b/crates/common/src/local_db/query/fetch_trades_by_tx/mod.rs @@ -0,0 +1,128 @@ +use crate::local_db::query::{SqlBuildError, SqlStatement, SqlValue}; +use alloy::primitives::{Address, B256}; + +const QUERY_TEMPLATE: &str = include_str!("query.sql"); + +const TAKE_ORDERS_CHAIN_IDS_CLAUSE: &str = "/*TAKE_ORDERS_CHAIN_IDS_CLAUSE*/"; +const TAKE_ORDERS_CHAIN_IDS_CLAUSE_BODY: &str = "AND t.chain_id IN ({list})"; +const TAKE_ORDERS_ORDERBOOKS_CLAUSE: &str = "/*TAKE_ORDERS_ORDERBOOKS_CLAUSE*/"; +const TAKE_ORDERS_ORDERBOOKS_CLAUSE_BODY: &str = "AND t.orderbook_address IN ({list})"; + +const CLEAR_EVENTS_CHAIN_IDS_CLAUSE: &str = "/*CLEAR_EVENTS_CHAIN_IDS_CLAUSE*/"; +const CLEAR_EVENTS_CHAIN_IDS_CLAUSE_BODY: &str = "AND c.chain_id IN ({list})"; +const CLEAR_EVENTS_ORDERBOOKS_CLAUSE: &str = "/*CLEAR_EVENTS_ORDERBOOKS_CLAUSE*/"; +const CLEAR_EVENTS_ORDERBOOKS_CLAUSE_BODY: &str = "AND c.orderbook_address IN ({list})"; + +#[derive(Debug, Clone)] +pub struct FetchTradesByTxArgs { + pub chain_ids: Vec, + pub orderbook_addresses: Vec
, + pub tx_hash: B256, +} + +pub fn build_fetch_trades_by_tx_stmt( + args: &FetchTradesByTxArgs, +) -> Result { + let mut stmt = SqlStatement::new(QUERY_TEMPLATE); + + stmt.push(SqlValue::from(args.tx_hash)); + + let mut chain_ids = args.chain_ids.clone(); + chain_ids.sort_unstable(); + chain_ids.dedup(); + + let mut orderbooks = args.orderbook_addresses.clone(); + orderbooks.sort(); + orderbooks.dedup(); + + let chain_ids_iter = || chain_ids.iter().cloned().map(SqlValue::from); + let orderbooks_iter = || orderbooks.iter().cloned().map(SqlValue::from); + + stmt.bind_list_clause( + TAKE_ORDERS_CHAIN_IDS_CLAUSE, + TAKE_ORDERS_CHAIN_IDS_CLAUSE_BODY, + chain_ids_iter(), + )?; + stmt.bind_list_clause( + CLEAR_EVENTS_CHAIN_IDS_CLAUSE, + CLEAR_EVENTS_CHAIN_IDS_CLAUSE_BODY, + chain_ids_iter(), + )?; + stmt.bind_list_clause( + TAKE_ORDERS_ORDERBOOKS_CLAUSE, + TAKE_ORDERS_ORDERBOOKS_CLAUSE_BODY, + orderbooks_iter(), + )?; + stmt.bind_list_clause( + CLEAR_EVENTS_ORDERBOOKS_CLAUSE, + CLEAR_EVENTS_ORDERBOOKS_CLAUSE_BODY, + orderbooks_iter(), + )?; + Ok(stmt) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::{ + hex, + primitives::{address, b256}, + }; + + #[test] + fn builds_with_chain_ids_and_tx_hash() { + let tx_hash = + b256!("0x00000000000000000000000000000000000000000000000000000000deadbeef"); + let stmt = build_fetch_trades_by_tx_stmt(&FetchTradesByTxArgs { + chain_ids: vec![137, 1, 137], + orderbook_addresses: vec![], + tx_hash, + }) + .unwrap(); + assert_eq!(stmt.params.len(), 5); + assert_eq!( + stmt.params[0], + SqlValue::Text(hex::encode_prefixed(tx_hash)) + ); + assert_eq!(stmt.params[1], SqlValue::U64(1)); + assert_eq!(stmt.params[2], SqlValue::U64(137)); + assert_eq!(stmt.params[3], SqlValue::U64(1)); + assert_eq!(stmt.params[4], SqlValue::U64(137)); + assert!(stmt.sql.contains("t.chain_id IN (?2, ?3)")); + assert!(stmt.sql.contains("c.chain_id IN (?4, ?5)")); + assert!(!stmt.sql.contains(TAKE_ORDERS_CHAIN_IDS_CLAUSE)); + assert!(!stmt.sql.contains(CLEAR_EVENTS_CHAIN_IDS_CLAUSE)); + } + + #[test] + fn builds_with_orderbook_address_filters() { + let tx_hash = + b256!("0x00000000000000000000000000000000000000000000000000000000deadbeef"); + let ob = address!("0x2f209e5b67a33b8fe96e28f24628df6da301c8eb"); + let stmt = build_fetch_trades_by_tx_stmt(&FetchTradesByTxArgs { + chain_ids: vec![137], + orderbook_addresses: vec![ob], + tx_hash, + }) + .unwrap(); + assert_eq!(stmt.params.len(), 5); + assert_eq!( + stmt.params[0], + SqlValue::Text(hex::encode_prefixed(tx_hash)) + ); + assert_eq!(stmt.params[1], SqlValue::U64(137)); + assert_eq!(stmt.params[2], SqlValue::U64(137)); + assert_eq!( + stmt.params[3], + SqlValue::Text(hex::encode_prefixed(ob)) + ); + assert_eq!( + stmt.params[4], + SqlValue::Text(hex::encode_prefixed(ob)) + ); + assert!(stmt.sql.contains("t.orderbook_address IN (?4)")); + assert!(stmt.sql.contains("c.orderbook_address IN (?5)")); + assert!(!stmt.sql.contains(TAKE_ORDERS_ORDERBOOKS_CLAUSE)); + assert!(!stmt.sql.contains(CLEAR_EVENTS_ORDERBOOKS_CLAUSE)); + } +} diff --git a/crates/common/src/local_db/query/fetch_trades_by_tx/query.sql b/crates/common/src/local_db/query/fetch_trades_by_tx/query.sql new file mode 100644 index 0000000000..97d40dfcab --- /dev/null +++ b/crates/common/src/local_db/query/fetch_trades_by_tx/query.sql @@ -0,0 +1,371 @@ +WITH +params AS ( + SELECT + ?1 AS transaction_hash +), +matching_take_orders AS ( + SELECT + 'take' AS trade_kind, + t.chain_id, + t.orderbook_address, + t.order_owner, + t.order_nonce, + t.transaction_hash, + t.log_index, + t.block_number, + t.block_timestamp, + t.sender AS transaction_sender, + t.input_io_index, + t.output_io_index, + t.taker_output AS input_delta, + FLOAT_NEGATE(t.taker_input) AS output_delta + FROM take_orders t + JOIN params p + ON t.transaction_hash = p.transaction_hash + WHERE 1 = 1 + /*TAKE_ORDERS_CHAIN_IDS_CLAUSE*/ + /*TAKE_ORDERS_ORDERBOOKS_CLAUSE*/ +), +matching_clears AS ( + SELECT + c.chain_id, + c.orderbook_address, + c.transaction_hash, + c.log_index, + c.block_number, + c.block_timestamp, + c.sender, + c.alice_order_hash, + c.bob_order_hash, + c.alice_input_io_index, + c.alice_output_io_index, + c.alice_input_vault_id, + c.alice_output_vault_id, + c.bob_input_io_index, + c.bob_output_io_index, + c.bob_input_vault_id, + c.bob_output_vault_id + FROM clear_v3_events c + JOIN params p + ON c.transaction_hash = p.transaction_hash + WHERE 1 = 1 + /*CLEAR_EVENTS_CHAIN_IDS_CLAUSE*/ + /*CLEAR_EVENTS_ORDERBOOKS_CLAUSE*/ +), +take_trades AS ( + SELECT + mt.trade_kind, + mt.chain_id, + mt.orderbook_address, + oe.order_hash, + mt.order_owner, + mt.order_nonce, + mt.transaction_hash, + mt.log_index, + mt.block_number, + mt.block_timestamp, + mt.transaction_sender, + io_in.vault_id AS input_vault_id, + io_in.token AS input_token, + mt.input_delta, + io_out.vault_id AS output_vault_id, + io_out.token AS output_token, + mt.output_delta + FROM matching_take_orders mt + JOIN order_events oe + ON oe.chain_id = mt.chain_id + AND oe.orderbook_address = mt.orderbook_address + AND oe.order_owner = mt.order_owner + AND oe.order_nonce = mt.order_nonce + AND oe.event_type = 'AddOrderV3' + AND ( + oe.block_number < mt.block_number + OR (oe.block_number = mt.block_number AND oe.log_index <= mt.log_index) + ) + AND NOT EXISTS ( + SELECT 1 + FROM order_events newer + WHERE newer.chain_id = oe.chain_id + AND newer.orderbook_address = oe.orderbook_address + AND newer.order_owner = oe.order_owner + AND newer.order_nonce = oe.order_nonce + AND newer.event_type = 'AddOrderV3' + AND ( + newer.block_number < mt.block_number + OR (newer.block_number = mt.block_number AND newer.log_index <= mt.log_index) + ) + AND ( + newer.block_number > oe.block_number + OR (newer.block_number = oe.block_number AND newer.log_index > oe.log_index) + ) + ) + JOIN order_ios io_in + ON io_in.chain_id = oe.chain_id + AND io_in.orderbook_address = oe.orderbook_address + AND io_in.transaction_hash = oe.transaction_hash + AND io_in.log_index = oe.log_index + AND io_in.io_index = mt.input_io_index + AND io_in.io_type = 'input' + JOIN order_ios io_out + ON io_out.chain_id = oe.chain_id + AND io_out.orderbook_address = oe.orderbook_address + AND io_out.transaction_hash = oe.transaction_hash + AND io_out.log_index = oe.log_index + AND io_out.io_index = mt.output_io_index + AND io_out.io_type = 'output' +), +clear_alice AS ( + SELECT DISTINCT + 'clear' AS trade_kind, + mc.chain_id, + mc.orderbook_address, + oe.order_hash, + oe.order_owner, + oe.order_nonce, + mc.transaction_hash, + mc.log_index, + mc.block_number, + mc.block_timestamp, + mc.sender AS transaction_sender, + mc.alice_input_vault_id AS input_vault_id, + io_in.token AS input_token, + a.alice_input AS input_delta, + mc.alice_output_vault_id AS output_vault_id, + io_out.token AS output_token, + FLOAT_NEGATE(a.alice_output) AS output_delta + FROM matching_clears mc + JOIN order_events oe + ON oe.chain_id = mc.chain_id + AND oe.orderbook_address = mc.orderbook_address + AND oe.order_hash = mc.alice_order_hash + AND oe.event_type = 'AddOrderV3' + AND ( + oe.block_number < mc.block_number + OR (oe.block_number = mc.block_number AND oe.log_index <= mc.log_index) + ) + AND NOT EXISTS ( + SELECT 1 + FROM order_events newer + WHERE newer.chain_id = oe.chain_id + AND newer.orderbook_address = oe.orderbook_address + AND newer.order_hash = oe.order_hash + AND newer.event_type = 'AddOrderV3' + AND ( + newer.block_number < mc.block_number + OR (newer.block_number = mc.block_number AND newer.log_index <= mc.log_index) + ) + AND ( + newer.block_number > oe.block_number + OR (newer.block_number = oe.block_number AND newer.log_index > oe.log_index) + ) + ) + JOIN after_clear_v2_events a + ON a.chain_id = mc.chain_id + AND a.orderbook_address = mc.orderbook_address + AND a.transaction_hash = mc.transaction_hash + AND a.log_index = ( + SELECT MIN(ac.log_index) + FROM after_clear_v2_events ac + WHERE ac.chain_id = mc.chain_id + AND ac.orderbook_address = mc.orderbook_address + AND ac.transaction_hash = mc.transaction_hash + AND ac.log_index > mc.log_index + ) + JOIN order_ios io_in + ON io_in.chain_id = oe.chain_id + AND io_in.orderbook_address = oe.orderbook_address + AND io_in.transaction_hash = oe.transaction_hash + AND io_in.log_index = oe.log_index + AND io_in.io_index = mc.alice_input_io_index + AND io_in.io_type = 'input' + JOIN order_ios io_out + ON io_out.chain_id = oe.chain_id + AND io_out.orderbook_address = oe.orderbook_address + AND io_out.transaction_hash = oe.transaction_hash + AND io_out.log_index = oe.log_index + AND io_out.io_index = mc.alice_output_io_index + AND io_out.io_type = 'output' +), +clear_bob AS ( + SELECT DISTINCT + 'clear' AS trade_kind, + mc.chain_id, + mc.orderbook_address, + oe.order_hash, + oe.order_owner, + oe.order_nonce, + mc.transaction_hash, + mc.log_index, + mc.block_number, + mc.block_timestamp, + mc.sender AS transaction_sender, + mc.bob_input_vault_id AS input_vault_id, + io_in.token AS input_token, + a.bob_input AS input_delta, + mc.bob_output_vault_id AS output_vault_id, + io_out.token AS output_token, + FLOAT_NEGATE(a.bob_output) AS output_delta + FROM matching_clears mc + JOIN order_events oe + ON oe.chain_id = mc.chain_id + AND oe.orderbook_address = mc.orderbook_address + AND oe.order_hash = mc.bob_order_hash + AND oe.event_type = 'AddOrderV3' + AND ( + oe.block_number < mc.block_number + OR (oe.block_number = mc.block_number AND oe.log_index <= mc.log_index) + ) + AND NOT EXISTS ( + SELECT 1 + FROM order_events newer + WHERE newer.chain_id = oe.chain_id + AND newer.orderbook_address = oe.orderbook_address + AND newer.order_hash = oe.order_hash + AND newer.event_type = 'AddOrderV3' + AND ( + newer.block_number < mc.block_number + OR (newer.block_number = mc.block_number AND newer.log_index <= mc.log_index) + ) + AND ( + newer.block_number > oe.block_number + OR (newer.block_number = oe.block_number AND newer.log_index > oe.log_index) + ) + ) + JOIN after_clear_v2_events a + ON a.chain_id = mc.chain_id + AND a.orderbook_address = mc.orderbook_address + AND a.transaction_hash = mc.transaction_hash + AND a.log_index = ( + SELECT MIN(ac.log_index) + FROM after_clear_v2_events ac + WHERE ac.chain_id = mc.chain_id + AND ac.orderbook_address = mc.orderbook_address + AND ac.transaction_hash = mc.transaction_hash + AND ac.log_index > mc.log_index + ) + JOIN order_ios io_in + ON io_in.chain_id = oe.chain_id + AND io_in.orderbook_address = oe.orderbook_address + AND io_in.transaction_hash = oe.transaction_hash + AND io_in.log_index = oe.log_index + AND io_in.io_index = mc.bob_input_io_index + AND io_in.io_type = 'input' + JOIN order_ios io_out + ON io_out.chain_id = oe.chain_id + AND io_out.orderbook_address = oe.orderbook_address + AND io_out.transaction_hash = oe.transaction_hash + AND io_out.log_index = oe.log_index + AND io_out.io_index = mc.bob_output_io_index + AND io_out.io_type = 'output' +), +clear_trades AS ( + SELECT * FROM clear_alice + UNION ALL + SELECT * FROM clear_bob +), +unioned_trades AS ( + SELECT * FROM take_trades + UNION ALL + SELECT * FROM clear_trades +), +trade_rows AS ( + SELECT + ut.trade_kind, + ut.chain_id, + ut.orderbook_address, + ut.order_hash, + ut.order_owner, + ut.order_nonce, + ut.transaction_hash, + ut.log_index, + ut.block_number, + ut.block_timestamp, + ut.transaction_sender, + ut.input_vault_id, + ut.input_token, + ut.input_delta, + ut.output_vault_id, + ut.output_token, + ut.output_delta + FROM unioned_trades ut +), +trade_with_snapshots AS ( + SELECT + tr.*, + mvb_in.balance AS input_base_balance, + mvb_in.last_block AS input_base_block, + mvb_in.last_log_index AS input_base_log_index, + mvb_out.balance AS output_base_balance, + mvb_out.last_block AS output_base_block, + mvb_out.last_log_index AS output_base_log_index + FROM trade_rows tr + LEFT JOIN running_vault_balances mvb_in + ON mvb_in.chain_id = tr.chain_id + AND mvb_in.orderbook_address = tr.orderbook_address + AND mvb_in.owner = tr.order_owner + AND mvb_in.token = tr.input_token + AND mvb_in.vault_id = tr.input_vault_id + LEFT JOIN running_vault_balances mvb_out + ON mvb_out.chain_id = tr.chain_id + AND mvb_out.orderbook_address = tr.orderbook_address + AND mvb_out.owner = tr.order_owner + AND mvb_out.token = tr.output_token + AND mvb_out.vault_id = tr.output_vault_id +) +SELECT + tws.chain_id, + tws.trade_kind, + tws.orderbook_address AS orderbook, + tws.order_hash, + tws.order_owner, + tws.order_nonce, + tws.transaction_hash, + tws.log_index, + tws.block_number, + tws.block_timestamp, + tws.transaction_sender, + tws.input_vault_id, + tws.input_token, + tok_in.name AS input_token_name, + tok_in.symbol AS input_token_symbol, + tok_in.decimals AS input_token_decimals, + tws.input_delta, + vbc_input.running_balance AS input_running_balance, + tws.output_vault_id, + tws.output_token, + tok_out.name AS output_token_name, + tok_out.symbol AS output_token_symbol, + tok_out.decimals AS output_token_decimals, + tws.output_delta, + vbc_output.running_balance AS output_running_balance, + ( + '0x' || + lower(replace(tws.transaction_hash, '0x', '')) || + printf('%016x', tws.log_index) + ) AS trade_id +FROM trade_with_snapshots tws +LEFT JOIN vault_balance_changes vbc_input + ON vbc_input.chain_id = tws.chain_id + AND vbc_input.orderbook_address = tws.orderbook_address + AND vbc_input.owner = tws.order_owner + AND vbc_input.token = tws.input_token + AND vbc_input.vault_id = tws.input_vault_id + AND vbc_input.block_number = tws.block_number + AND vbc_input.log_index = tws.log_index +LEFT JOIN vault_balance_changes vbc_output + ON vbc_output.chain_id = tws.chain_id + AND vbc_output.orderbook_address = tws.orderbook_address + AND vbc_output.owner = tws.order_owner + AND vbc_output.token = tws.output_token + AND vbc_output.vault_id = tws.output_vault_id + AND vbc_output.block_number = tws.block_number + AND vbc_output.log_index = tws.log_index +LEFT JOIN erc20_tokens tok_in + ON tok_in.chain_id = tws.chain_id + AND tok_in.orderbook_address = tws.orderbook_address + AND tok_in.token_address = tws.input_token +LEFT JOIN erc20_tokens tok_out + ON tok_out.chain_id = tws.chain_id + AND tok_out.orderbook_address = tws.orderbook_address + AND tok_out.token_address = tws.output_token +ORDER BY tws.block_timestamp DESC, tws.block_number DESC, tws.log_index DESC, tws.trade_kind; diff --git a/crates/common/src/local_db/query/mod.rs b/crates/common/src/local_db/query/mod.rs index 0d8a989e98..6d53db9365 100644 --- a/crates/common/src/local_db/query/mod.rs +++ b/crates/common/src/local_db/query/mod.rs @@ -9,6 +9,7 @@ pub mod fetch_erc20_tokens_by_addresses; pub mod fetch_last_synced_block; pub mod fetch_order_trades; pub mod fetch_order_trades_count; +pub mod fetch_trades_by_tx; pub mod fetch_order_vaults_volume; pub mod fetch_orders; pub(crate) mod fetch_orders_common; diff --git a/crates/common/src/raindex_client/local_db/query/fetch_trades_by_tx.rs b/crates/common/src/raindex_client/local_db/query/fetch_trades_by_tx.rs new file mode 100644 index 0000000000..c5567d928b --- /dev/null +++ b/crates/common/src/raindex_client/local_db/query/fetch_trades_by_tx.rs @@ -0,0 +1,51 @@ +use crate::local_db::query::fetch_order_trades::LocalDbOrderTrade; +use crate::local_db::query::fetch_trades_by_tx::{ + build_fetch_trades_by_tx_stmt, FetchTradesByTxArgs, +}; +use crate::local_db::query::{LocalDbQueryError, LocalDbQueryExecutor}; + +pub async fn fetch_trades_by_tx( + exec: &E, + args: FetchTradesByTxArgs, +) -> Result, LocalDbQueryError> { + let stmt = build_fetch_trades_by_tx_stmt(&args)?; + exec.query_json(&stmt).await +} + +#[cfg(all(test, target_family = "wasm"))] +mod wasm_tests { + use super::*; + use crate::raindex_client::local_db::executor::tests::create_sql_capturing_callback; + use crate::raindex_client::local_db::executor::JsCallbackExecutor; + use alloy::primitives::b256; + use std::cell::RefCell; + use std::rc::Rc; + use wasm_bindgen_test::*; + use wasm_bindgen_utils::prelude::*; + + #[wasm_bindgen_test] + async fn wrapper_uses_builder_sql_exactly() { + let tx_hash = + b256!("0x00000000000000000000000000000000000000000000000000000000deadbeef"); + let args = FetchTradesByTxArgs { + chain_ids: vec![137, 42161], + orderbook_addresses: vec![], + tx_hash, + }; + + let expected_stmt = build_fetch_trades_by_tx_stmt(&args).unwrap(); + + let store = Rc::new(RefCell::new(( + String::new(), + wasm_bindgen::JsValue::UNDEFINED, + ))); + let callback = create_sql_capturing_callback("[]", store.clone()); + let exec = JsCallbackExecutor::from_ref(&callback); + + let res = fetch_trades_by_tx(&exec, args).await; + assert!(res.is_ok()); + + let captured = store.borrow().clone(); + assert_eq!(captured.0, expected_stmt.sql); + } +} diff --git a/crates/common/src/raindex_client/local_db/query/mod.rs b/crates/common/src/raindex_client/local_db/query/mod.rs index c57dcc15ef..43fad7d2c5 100644 --- a/crates/common/src/raindex_client/local_db/query/mod.rs +++ b/crates/common/src/raindex_client/local_db/query/mod.rs @@ -5,6 +5,7 @@ pub mod fetch_erc20_tokens_by_addresses; pub mod fetch_last_synced_block; pub mod fetch_order_trades; pub mod fetch_order_trades_count; +pub mod fetch_trades_by_tx; pub mod fetch_order_vaults_volume; pub mod fetch_orders; pub mod fetch_orders_count; diff --git a/crates/common/src/types/order_takes_list_flattened.rs b/crates/common/src/types/order_takes_list_flattened.rs index 411febea20..6d1da1f6ff 100644 --- a/crates/common/src/types/order_takes_list_flattened.rs +++ b/crates/common/src/types/order_takes_list_flattened.rs @@ -82,6 +82,7 @@ mod tests { order: SgTradeStructPartialOrder { id: SgBytes("orderPartial001".to_string()), order_hash: SgBytes("orderHash001".to_string()), + owner: SgBytes("0xowner001".to_string()), }, input_vault_balance_change: SgTradeVaultBalanceChange { id: SgBytes("inputVBC001".to_string()), From 33ea673fcc360fb6008a20c89451bff3077a00e7 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 12:19:53 +0300 Subject: [PATCH 04/12] feat: refactor trades API to return RaindexTradesListResult Restructure get_trades_list to return a combined result with trades, totalCount, and summary instead of separate calls. Add get_trades_for_transaction for querying trades by tx hash across local DB and subgraph sources. Use HashMap for O(1) subgraph name to chain_id lookup instead of linear scan. --- .../src/raindex_client/local_db/orders.rs | 2 +- crates/common/src/raindex_client/trades.rs | 615 ++++++++++++------ 2 files changed, 414 insertions(+), 203 deletions(-) diff --git a/crates/common/src/raindex_client/local_db/orders.rs b/crates/common/src/raindex_client/local_db/orders.rs index 5708713d13..f38e6703cd 100644 --- a/crates/common/src/raindex_client/local_db/orders.rs +++ b/crates/common/src/raindex_client/local_db/orders.rs @@ -192,7 +192,7 @@ impl OrdersDataSource for LocalDbOrders<'_> { local_trades .into_iter() - .map(|trade| RaindexTrade::try_from_local_db_trade(ob_id.chain_id, trade)) + .map(RaindexTrade::try_from_local_db_trade) .collect() } diff --git a/crates/common/src/raindex_client/trades.rs b/crates/common/src/raindex_client/trades.rs index 2f1d30ac85..686c87000e 100644 --- a/crates/common/src/raindex_client/trades.rs +++ b/crates/common/src/raindex_client/trades.rs @@ -4,14 +4,19 @@ use super::ClientRef; use super::*; use crate::local_db::query::fetch_order_trades::LocalDbOrderTrade; use crate::local_db::OrderbookIdentifier; -use crate::raindex_client::QuerySource; use crate::raindex_client::{ + local_db::query::fetch_trades_by_tx::FetchTradesByTxArgs, orders::RaindexOrder, transactions::RaindexTransaction, vaults::{LocalTradeBalanceInfo, LocalTradeTokenInfo, RaindexVaultBalanceChange}, }; +use crate::raindex_client::local_db::query::fetch_trades_by_tx::fetch_trades_by_tx; use alloy::primitives::{Address, Bytes, B256, U256}; -use rain_orderbook_subgraph_client::types::{common::SgTrade, Id}; +use rain_math_float::Float; +use rain_orderbook_subgraph_client::{ + types::{common::SgTrade, Id}, + MultiOrderbookSubgraphClient, +}; use std::str::FromStr; #[cfg(target_family = "wasm")] use wasm_bindgen_utils::prelude::js_sys::BigInt; @@ -21,12 +26,16 @@ use wasm_bindgen_utils::prelude::js_sys::BigInt; #[wasm_bindgen] pub struct RaindexTrade { id: Bytes, - order_hash: Bytes, + chain_id: u32, + orderbook: Address, + order_hash: B256, + owner: Address, transaction: RaindexTransaction, input_vault_balance_change: RaindexVaultBalanceChange, output_vault_balance_change: RaindexVaultBalanceChange, timestamp: U256, - orderbook: Address, + io_ratio: Float, + formatted_io_ratio: String, } #[cfg(target_family = "wasm")] #[wasm_bindgen] @@ -35,10 +44,22 @@ impl RaindexTrade { pub fn id(&self) -> String { self.id.to_string() } + #[wasm_bindgen(getter = chainId)] + pub fn chain_id(&self) -> u32 { + self.chain_id + } + #[wasm_bindgen(getter, unchecked_return_type = "Address")] + pub fn orderbook(&self) -> String { + self.orderbook.to_string() + } #[wasm_bindgen(getter = orderHash, unchecked_return_type = "Hex")] pub fn order_hash(&self) -> String { self.order_hash.to_string() } + #[wasm_bindgen(getter, unchecked_return_type = "Address")] + pub fn owner(&self) -> String { + self.owner.to_string() + } #[wasm_bindgen(getter)] pub fn transaction(&self) -> RaindexTransaction { self.transaction.clone() @@ -56,9 +77,13 @@ impl RaindexTrade { BigInt::from_str(&self.timestamp.to_string()) .map_err(|e| RaindexError::JsError(e.to_string().into())) } - #[wasm_bindgen(getter, unchecked_return_type = "Address")] - pub fn orderbook(&self) -> String { - self.orderbook.to_string() + #[wasm_bindgen(getter = ioRatio)] + pub fn io_ratio(&self) -> Float { + self.io_ratio + } + #[wasm_bindgen(getter = formattedIoRatio)] + pub fn formatted_io_ratio(&self) -> String { + self.formatted_io_ratio.clone() } } #[cfg(not(target_family = "wasm"))] @@ -66,8 +91,17 @@ impl RaindexTrade { pub fn id(&self) -> Bytes { self.id.clone() } - pub fn order_hash(&self) -> Bytes { - self.order_hash.clone() + pub fn chain_id(&self) -> u32 { + self.chain_id + } + pub fn orderbook(&self) -> Address { + self.orderbook + } + pub fn order_hash(&self) -> B256 { + self.order_hash + } + pub fn owner(&self) -> Address { + self.owner } pub fn transaction(&self) -> RaindexTransaction { self.transaction.clone() @@ -81,8 +115,151 @@ impl RaindexTrade { pub fn timestamp(&self) -> U256 { self.timestamp } - pub fn orderbook(&self) -> Address { - self.orderbook + pub fn io_ratio(&self) -> Float { + self.io_ratio + } + pub fn formatted_io_ratio(&self) -> &str { + &self.formatted_io_ratio + } +} + +#[wasm_export] +impl RaindexClient { + /// Fetches all trades from a specific transaction + /// + /// Queries either local DB or subgraph based on the configured data source + /// for each chain, using the classify_chains pattern. + /// + /// ## Examples + /// + /// ```javascript + /// const result = await client.getTradesForTransaction( + /// undefined, + /// undefined, + /// "0xabcdef..." + /// ); + /// if (result.error) { + /// console.error("Error:", result.error.readableMsg); + /// return; + /// } + /// const { trades, totalCount, summary } = result.value; + /// ``` + #[wasm_export( + js_name = "getTradesForTransaction", + return_description = "Trades list result with total count and summary", + unchecked_return_type = "RaindexTradesListResult", + preserve_js_class + )] + pub async fn get_trades_for_transaction_wasm_binding( + &self, + #[wasm_export( + js_name = "chainIds", + param_description = "Optional chain IDs to filter networks (queries all if not specified)" + )] + chain_ids: Option, + #[wasm_export( + js_name = "orderbookAddresses", + param_description = "Optional orderbook addresses to filter results" + )] + orderbook_addresses: Option>, + #[wasm_export( + js_name = "txHash", + param_description = "Transaction hash", + unchecked_param_type = "Hex" + )] + tx_hash: String, + ) -> Result { + let tx_hash = B256::from_str(&tx_hash)?; + let orderbook_addresses = orderbook_addresses + .map(|addresses| { + addresses + .into_iter() + .map(|address| Address::from_str(&address)) + .collect::, _>>() + }) + .transpose()?; + self.get_trades_for_transaction(chain_ids, orderbook_addresses, tx_hash) + .await + } + +} +impl RaindexClient { + pub async fn get_trades_for_transaction( + &self, + chain_ids: Option, + orderbook_addresses: Option>, + tx_hash: B256, + ) -> Result { + let ids = chain_ids.map(|ChainIds(ids)| ids); + let (local_db, local_ids, sg_ids) = self.classify_chains(ids)?; + let orderbook_addresses_for_local_db = orderbook_addresses.clone().unwrap_or_default(); + + let mut all_trades = Vec::new(); + + if let Some(db) = local_db.filter(|_| !local_ids.is_empty()) { + let trades = fetch_trades_by_tx( + &db, + FetchTradesByTxArgs { + chain_ids: local_ids, + orderbook_addresses: orderbook_addresses_for_local_db, + tx_hash, + }, + ) + .await?; + let raindex_trades: Vec = trades + .into_iter() + .map(RaindexTrade::try_from_local_db_trade) + .collect::>()?; + all_trades.extend(raindex_trades); + } + + if !sg_ids.is_empty() { + let multi_subgraph_args = self.get_multi_subgraph_args(Some(sg_ids))?; + let orderbook_in = orderbook_addresses + .as_deref() + .filter(|addresses| !addresses.is_empty()) + .map(|addresses| { + addresses + .iter() + .map(|address| address.to_string().to_lowercase()) + .collect::>() + }); + if !multi_subgraph_args.is_empty() { + let name_to_chain_id: std::collections::HashMap<&str, u32> = multi_subgraph_args + .iter() + .flat_map(|(chain_id, args)| { + args.iter().map(|arg| (arg.name.as_str(), *chain_id)) + }) + .collect(); + let client = MultiOrderbookSubgraphClient::new( + multi_subgraph_args.values().flatten().cloned().collect(), + ); + let sg_trades = client + .trades_by_transaction(tx_hash.to_string(), orderbook_in) + .await; + for trade_with_name in sg_trades { + let chain_id = name_to_chain_id + .get(trade_with_name.subgraph_name.as_str()) + .copied() + .ok_or(RaindexError::SubgraphNotFound( + trade_with_name.subgraph_name.clone(), + trade_with_name.trade.id.0.clone(), + ))?; + let trade = + RaindexTrade::try_from_sg_trade(chain_id, trade_with_name.trade)?; + all_trades.push(trade); + } + } + } + + let total_count = all_trades.len() as u64; + let summary = RaindexTradeSummary::from_trades(&all_trades)?; + + Ok(RaindexTradesListResult { + trades: all_trades, + total_count, + summary, + }) } } @@ -91,7 +268,7 @@ impl RaindexOrder { /// Fetches trade history with optional time filtering /// /// Retrieves a chronological list of trades executed by an order within - /// an optional time range. + /// an optional time range, along with the total count and summary. /// /// ## Examples /// @@ -101,13 +278,13 @@ impl RaindexOrder { /// console.error("Cannot fetch trades:", result.error.readableMsg); /// return; /// } - /// const trades = result.value; - /// // Do something with the trades + /// const { trades, totalCount, summary } = result.value; /// ``` #[wasm_export( js_name = "getTradesList", - return_description = "Array of trade records with complete details", - unchecked_return_type = "RaindexTrade[]" + return_description = "Trades list result with total count and summary", + unchecked_return_type = "RaindexTradesListResult", + preserve_js_class )] pub async fn get_trades_list( &self, @@ -126,7 +303,7 @@ impl RaindexOrder { param_description = "Optional page number (defaults to 1)" )] page: Option, - ) -> Result, RaindexError> { + ) -> Result { let chain_id = self.chain_id(); #[cfg(target_family = "wasm")] let orderbook = Address::from_str(&self.orderbook())?; @@ -141,20 +318,44 @@ impl RaindexOrder { let ob_id = OrderbookIdentifier::new(chain_id, orderbook); let raindex_client = self.get_raindex_client(); - match raindex_client.query_source(chain_id) { + let (trades, total_count) = match raindex_client.query_source(chain_id) { QuerySource::LocalDb(local_db) => { let local_source = LocalDbOrders::new(&local_db, ClientRef::clone(&raindex_client)); - local_source + let trades = local_source .trades_list(&ob_id, &order_hash, start_timestamp, end_timestamp, page) - .await + .await?; + let total_count = if page.is_some() { + local_source + .trades_count(&ob_id, &order_hash, start_timestamp, end_timestamp) + .await? + } else { + trades.len() as u64 + }; + (trades, total_count) } QuerySource::Subgraph => { let subgraph_source = SubgraphOrders::new(&raindex_client); - subgraph_source + let trades = subgraph_source .trades_list(&ob_id, &order_hash, start_timestamp, end_timestamp, page) - .await + .await?; + let total_count = if page.is_some() { + subgraph_source + .trades_count(&ob_id, &order_hash, start_timestamp, end_timestamp) + .await? + } else { + trades.len() as u64 + }; + (trades, total_count) } - } + }; + + let summary = RaindexTradeSummary::from_trades(&trades)?; + + Ok(RaindexTradesListResult { + trades, + total_count, + summary, + }) } /// Fetches detailed information for a specific trade @@ -171,7 +372,6 @@ impl RaindexOrder { /// return; /// } /// const trade = result.value; - /// // Do something with the trade /// ``` #[wasm_export( js_name = "getTradeDetail", @@ -190,70 +390,6 @@ impl RaindexOrder { let trade_id = Bytes::from_str(&trade_id)?; self.get_trade_detail(trade_id).await } - - /// Counts total trades for an order within a time range - /// - /// Efficiently counts the total number of trades executed by an order without - /// fetching all trade details. - /// - /// ## Examples - /// - /// ```javascript - /// const result = await order.getTradeCount(); - /// if (result.error) { - /// console.error("Cannot count trades:", result.error.readableMsg); - /// return; - /// } - /// const count = result.value; - /// // Do something with the count - /// ``` - #[wasm_export( - js_name = "getTradeCount", - return_description = "Total trade count as number", - unchecked_return_type = "number" - )] - pub async fn get_trade_count( - &self, - #[wasm_export( - js_name = "startTimestamp", - param_description = "Optional start time filter (Unix timestamp in seconds)" - )] - start_timestamp: Option, - #[wasm_export( - js_name = "endTimestamp", - param_description = "Optional end time filter (Unix timestamp in seconds)" - )] - end_timestamp: Option, - ) -> Result { - let chain_id = self.chain_id(); - #[cfg(target_family = "wasm")] - let orderbook = Address::from_str(&self.orderbook())?; - #[cfg(not(target_family = "wasm"))] - let orderbook = self.orderbook(); - - #[cfg(target_family = "wasm")] - let order_hash = B256::from_str(&self.order_hash())?; - #[cfg(not(target_family = "wasm"))] - let order_hash = self.order_hash(); - - let ob_id = OrderbookIdentifier::new(chain_id, orderbook); - let raindex_client = self.get_raindex_client(); - - match raindex_client.query_source(chain_id) { - QuerySource::LocalDb(local_db) => { - let local_source = LocalDbOrders::new(&local_db, ClientRef::clone(&raindex_client)); - local_source - .trades_count(&ob_id, &order_hash, start_timestamp, end_timestamp) - .await - } - QuerySource::Subgraph => { - let subgraph_source = SubgraphOrders::new(&raindex_client); - subgraph_source - .trades_count(&ob_id, &order_hash, start_timestamp, end_timestamp) - .await - } - } - } } impl RaindexOrder { pub async fn get_trade_detail(&self, trade_id: Bytes) -> Result { @@ -269,29 +405,38 @@ impl RaindexOrder { impl RaindexTrade { pub fn try_from_sg_trade(chain_id: u32, trade: SgTrade) -> Result { + let input_vault_balance_change = + RaindexVaultBalanceChange::try_from_sg_trade_balance_change( + chain_id, + trade.input_vault_balance_change, + )?; + let output_vault_balance_change = + RaindexVaultBalanceChange::try_from_sg_trade_balance_change( + chain_id, + trade.output_vault_balance_change, + )?; + + let neg_output = Float::zero()?.sub(output_vault_balance_change.amount())?; + let io_ratio = input_vault_balance_change.amount().div(neg_output)?; + let formatted_io_ratio = io_ratio.format()?; + Ok(RaindexTrade { id: Bytes::from_str(&trade.id.0)?, - order_hash: Bytes::from_str(&trade.order.order_hash.0)?, + chain_id, + orderbook: Address::from_str(&trade.orderbook.id.0)?, + order_hash: B256::from_str(&trade.order.order_hash.0)?, + owner: Address::from_str(&trade.order.owner.0)?, transaction: RaindexTransaction::try_from(trade.trade_event.transaction)?, - input_vault_balance_change: - RaindexVaultBalanceChange::try_from_sg_trade_balance_change( - chain_id, - trade.input_vault_balance_change, - )?, - output_vault_balance_change: - RaindexVaultBalanceChange::try_from_sg_trade_balance_change( - chain_id, - trade.output_vault_balance_change, - )?, + input_vault_balance_change, + output_vault_balance_change, timestamp: U256::from_str(&trade.timestamp.0)?, - orderbook: Address::from_str(&trade.orderbook.id.0)?, + io_ratio, + formatted_io_ratio, }) } - pub(crate) fn try_from_local_db_trade( - chain_id: u32, - trade: LocalDbOrderTrade, - ) -> Result { + pub(crate) fn try_from_local_db_trade(trade: LocalDbOrderTrade) -> Result { + let chain_id = trade.chain_id; let transaction = RaindexTransaction::from_local_parts( trade.transaction_hash, trade.transaction_sender, @@ -337,18 +482,161 @@ impl RaindexTrade { trade.block_timestamp, )?; + let neg_output = Float::zero()?.sub(output_change.amount())?; + let io_ratio = input_change.amount().div(neg_output)?; + let formatted_io_ratio = io_ratio.format()?; + Ok(RaindexTrade { id: Bytes::from_str(&trade.trade_id)?, - order_hash: trade.order_hash.into(), + chain_id, + orderbook: trade.orderbook, + order_hash: trade.order_hash, + owner: trade.order_owner, transaction, input_vault_balance_change: input_change, output_vault_balance_change: output_change, timestamp: U256::from(trade.block_timestamp), - orderbook: trade.orderbook, + io_ratio, + formatted_io_ratio, + }) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[wasm_bindgen] +pub struct RaindexTradeSummary { + total_input: Float, + formatted_total_input: String, + total_output: Float, + formatted_total_output: String, + average_io_ratio: Float, + formatted_average_io_ratio: String, +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen] +impl RaindexTradeSummary { + #[wasm_bindgen(getter = totalInput)] + pub fn total_input(&self) -> Float { + self.total_input + } + #[wasm_bindgen(getter = formattedTotalInput)] + pub fn formatted_total_input(&self) -> String { + self.formatted_total_input.clone() + } + #[wasm_bindgen(getter = totalOutput)] + pub fn total_output(&self) -> Float { + self.total_output + } + #[wasm_bindgen(getter = formattedTotalOutput)] + pub fn formatted_total_output(&self) -> String { + self.formatted_total_output.clone() + } + #[wasm_bindgen(getter = averageIoRatio)] + pub fn average_io_ratio(&self) -> Float { + self.average_io_ratio + } + #[wasm_bindgen(getter = formattedAverageIoRatio)] + pub fn formatted_average_io_ratio(&self) -> String { + self.formatted_average_io_ratio.clone() + } +} + +#[cfg(not(target_family = "wasm"))] +impl RaindexTradeSummary { + pub fn total_input(&self) -> Float { + self.total_input + } + pub fn formatted_total_input(&self) -> &str { + &self.formatted_total_input + } + pub fn total_output(&self) -> Float { + self.total_output + } + pub fn formatted_total_output(&self) -> &str { + &self.formatted_total_output + } + pub fn average_io_ratio(&self) -> Float { + self.average_io_ratio + } + pub fn formatted_average_io_ratio(&self) -> &str { + &self.formatted_average_io_ratio + } +} + +impl RaindexTradeSummary { + pub fn from_trades(trades: &[RaindexTrade]) -> Result { + let mut total_input = Float::zero()?; + let mut total_output = Float::zero()?; + + for trade in trades { + total_input = total_input.add(trade.input_vault_balance_change.amount())?; + let neg_output = + Float::zero()?.sub(trade.output_vault_balance_change.amount())?; + total_output = total_output.add(neg_output)?; + } + + let formatted_total_input = total_input.format()?; + let formatted_total_output = total_output.format()?; + + let average_io_ratio = if total_output.eq(Float::zero()?).unwrap_or(true) { + Float::zero()? + } else { + total_input.div(total_output)? + }; + let formatted_average_io_ratio = average_io_ratio.format()?; + + Ok(RaindexTradeSummary { + total_input, + formatted_total_input, + total_output, + formatted_total_output, + average_io_ratio, + formatted_average_io_ratio, }) } } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[wasm_bindgen] +pub struct RaindexTradesListResult { + trades: Vec, + total_count: u64, + summary: RaindexTradeSummary, +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen] +impl RaindexTradesListResult { + #[wasm_bindgen(getter, unchecked_return_type = "RaindexTrade[]")] + pub fn trades(&self) -> Vec { + self.trades.clone() + } + #[wasm_bindgen(getter = totalCount)] + pub fn total_count(&self) -> u64 { + self.total_count + } + #[wasm_bindgen(getter)] + pub fn summary(&self) -> RaindexTradeSummary { + self.summary.clone() + } +} + +#[cfg(not(target_family = "wasm"))] +impl RaindexTradesListResult { + pub fn trades(&self) -> &[RaindexTrade] { + &self.trades + } + pub fn total_count(&self) -> u64 { + self.total_count + } + pub fn summary(&self) -> &RaindexTradeSummary { + &self.summary + } +} + #[cfg(test)] mod test_helpers { #[cfg(target_family = "wasm")] @@ -481,6 +769,7 @@ mod test_helpers { }; let trade = LocalDbOrderTrade { + chain_id: CHAIN_ID, trade_kind: "take".into(), orderbook: orderbook_address, order_hash: order_hash.clone(), @@ -651,8 +940,10 @@ mod test_helpers { .await .unwrap(); - let trades = order.get_trades_list(None, None, None).await.unwrap(); + let result = order.get_trades_list(None, None, None).await.unwrap(); + assert_eq!(result.total_count(), 1); + let trades = result.trades(); assert_eq!(trades.len(), 1); let trade = trades.first().unwrap(); @@ -722,41 +1013,6 @@ mod test_helpers { ); } - #[wasm_bindgen_test] - async fn test_get_trade_count_local_db_path() { - let fixture = build_local_trade_fixture( - b256!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - 3, - 7, - ); - - let callback = make_local_db_trades_callback( - vec![fixture.order.clone()], - vec![fixture.input_vault.clone(), fixture.output_vault.clone()], - vec![fixture.trade.clone()], - 7, - ); - let client = new_test_client_with_db_callback( - vec![get_local_db_test_yaml()], - callback, - vec![42161], - ); - - let order = client - .get_order_by_hash( - &OrderbookIdentifier::new(42161, fixture.orderbook_address), - fixture.order_hash.clone(), - ) - .await - .unwrap(); - - let count = order - .get_trade_count(Some(1_699_999_900), Some(1_700_000_900)) - .await - .unwrap(); - - assert_eq!(count, 7); - } } #[cfg(not(target_family = "wasm"))] @@ -938,7 +1194,8 @@ mod test_helpers { }, "order": { "id": "0x0123", - "orderHash": "0x0123" + "orderHash": "0x0123", + "owner": "0x0000000000000000000000000000000000000000" }, "inputVaultBalanceChange": { "id": "0x0123", @@ -1028,7 +1285,8 @@ mod test_helpers { }, "order": { "id": "0x0234", - "orderHash": "0x0234" + "orderHash": "0x0234", + "owner": "0x0000000000000000000000000000000000000001" }, "inputVaultBalanceChange": { "id": "0x0234", @@ -1121,7 +1379,9 @@ mod test_helpers { ) .await .unwrap(); - let trades = order.get_trades_list(None, None, None).await.unwrap(); + let result = order.get_trades_list(None, None, None).await.unwrap(); + assert_eq!(result.total_count(), 2); + let trades = result.trades(); assert_eq!(trades.len(), 2); let tx_hash = @@ -1261,7 +1521,10 @@ mod test_helpers { trade1.orderbook(), Address::from_str("0x1234567890abcdef1234567890abcdef12345678").unwrap() ); - assert_eq!(trade1.order_hash(), Bytes::from_str("0x0123").unwrap()); + assert_eq!( + trade1.order_hash(), + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000123").unwrap() + ); let trade2 = trades[1].clone(); assert_eq!(trade2.id(), Bytes::from_str("0x0234").unwrap()); @@ -1439,63 +1702,11 @@ mod test_helpers { trade.orderbook(), Address::from_str("0x1234567890abcdef1234567890abcdef12345678").unwrap() ); - assert_eq!(trade.order_hash(), Bytes::from_str("0x0123").unwrap()); + assert_eq!( + trade.order_hash(), + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000123").unwrap() + ); } - #[tokio::test] - async fn test_get_order_trades_count() { - let sg_server = MockServer::start_async().await; - sg_server.mock(|when, then| { - when.path("/sg") - .body_contains("\"first\":200") - .body_contains("\"skip\":0"); - then.status(200).json_body_obj(&json!({ - "data": { - "trades": get_trades_json() - } - })); - }); - sg_server.mock(|when, then| { - when.path("/sg") - .body_contains("\"first\":200") - .body_contains("\"skip\":200"); - then.status(200).json_body_obj(&json!({ - "data": { "trades": [] } - })); - }); - sg_server.mock(|when, then| { - when.path("/sg").body_contains("SgOrderDetailByHashQuery"); - then.status(200).json_body_obj(&json!({ - "data": { - "orders": [get_order1_json()] - } - })); - }); - - let raindex_client = RaindexClient::new( - vec![get_test_yaml( - &sg_server.url("/sg"), - "http://localhost:3000", - "http://localhost:3000", - "http://localhost:3000", - )], - None, - None, - ) - .await - .unwrap(); - let order = raindex_client - .get_order_by_hash( - &OrderbookIdentifier::new( - 1, - Address::from_str(CHAIN_ID_1_ORDERBOOK_ADDRESS).unwrap(), - ), - b256!("0x0000000000000000000000000000000000000000000000000000000000000123"), - ) - .await - .unwrap(); - let count = order.get_trade_count(None, None).await.unwrap(); - assert_eq!(count, 2); - } } } From fa43e5367df98f8e56ef43d1324636a3ba62ef02 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 12:20:00 +0300 Subject: [PATCH 05/12] feat: update frontend to use new RaindexTradesListResult structure Adapt OrderTradesListTable to consume the combined trades list result instead of making separate trades and count calls. Update OrderTradesChart to access .trades from the result. Update all related tests to wrap mock data in the new result structure. --- .../test/js_api/raindexClient.test.ts | 81 ++++++------------- .../__tests__/OrderTradesListTable.test.ts | 23 +++--- .../components/charts/OrderTradesChart.svelte | 2 +- .../tables/OrderTradesListTable.svelte | 39 ++++----- 4 files changed, 54 insertions(+), 91 deletions(-) diff --git a/packages/orderbook/test/js_api/raindexClient.test.ts b/packages/orderbook/test/js_api/raindexClient.test.ts index d777a9461d..61f882a479 100644 --- a/packages/orderbook/test/js_api/raindexClient.test.ts +++ b/packages/orderbook/test/js_api/raindexClient.test.ts @@ -1028,78 +1028,80 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f await raindexClient.getOrderByHash(1, CHAIN_ID_1_ORDERBOOK_ADDRESS, BYTES32_0123) ); const result = extractWasmEncodedData(await order.getTradesList()); - assert.equal(result.length, 1); - assert.equal(result[0].id, mockOrderTradesList[0].id); - assert.equal(result[0].orderHash, mockOrderTradesList[0].order.orderHash); - assert.equal(result[0].timestamp, BigInt(mockOrderTradesList[0].timestamp)); - assert.equal(result[0].orderbook, mockOrderTradesList[0].orderbook.id.toLowerCase()); + assert.equal(result.trades.length, 1); + assert.equal(result.totalCount, 1); + assert.ok(result.summary); + assert.equal(result.trades[0].id, mockOrderTradesList[0].id); + assert.equal(result.trades[0].orderHash, mockOrderTradesList[0].order.orderHash); + assert.equal(result.trades[0].timestamp, BigInt(mockOrderTradesList[0].timestamp)); + assert.equal(result.trades[0].orderbook, mockOrderTradesList[0].orderbook.id.toLowerCase()); assert.equal( - result[0].outputVaultBalanceChange.amount, + result.trades[0].outputVaultBalanceChange.amount, mockOrderTradesList[0].outputVaultBalanceChange.amount ); assert.equal( - result[0].outputVaultBalanceChange.vaultId, + result.trades[0].outputVaultBalanceChange.vaultId, BigInt(mockOrderTradesList[0].outputVaultBalanceChange.vault.vaultId) ); assert.equal( - result[0].outputVaultBalanceChange.token.id, + result.trades[0].outputVaultBalanceChange.token.id, mockOrderTradesList[0].outputVaultBalanceChange.vault.token.id ); assert.equal( - result[0].outputVaultBalanceChange.token.address, + result.trades[0].outputVaultBalanceChange.token.address, mockOrderTradesList[0].outputVaultBalanceChange.vault.token.address ); assert.equal( - result[0].outputVaultBalanceChange.token.name, + result.trades[0].outputVaultBalanceChange.token.name, mockOrderTradesList[0].outputVaultBalanceChange.vault.token.name ); assert.equal( - result[0].outputVaultBalanceChange.token.symbol, + result.trades[0].outputVaultBalanceChange.token.symbol, mockOrderTradesList[0].outputVaultBalanceChange.vault.token.symbol ); assert.equal( - result[0].outputVaultBalanceChange.token.decimals, + result.trades[0].outputVaultBalanceChange.token.decimals, BigInt(mockOrderTradesList[0].outputVaultBalanceChange.vault.token.decimals ?? 0) ); assert.equal( - result[0].inputVaultBalanceChange.amount, + result.trades[0].inputVaultBalanceChange.amount, mockOrderTradesList[0].inputVaultBalanceChange.amount ); assert.equal( - result[0].inputVaultBalanceChange.vaultId, + result.trades[0].inputVaultBalanceChange.vaultId, BigInt(mockOrderTradesList[0].inputVaultBalanceChange.vault.vaultId) ); assert.equal( - result[0].inputVaultBalanceChange.token.id, + result.trades[0].inputVaultBalanceChange.token.id, mockOrderTradesList[0].inputVaultBalanceChange.vault.token.id ); assert.equal( - result[0].inputVaultBalanceChange.token.address, + result.trades[0].inputVaultBalanceChange.token.address, mockOrderTradesList[0].inputVaultBalanceChange.vault.token.address ); assert.equal( - result[0].inputVaultBalanceChange.token.name, + result.trades[0].inputVaultBalanceChange.token.name, mockOrderTradesList[0].inputVaultBalanceChange.vault.token.name ); assert.equal( - result[0].inputVaultBalanceChange.token.symbol, + result.trades[0].inputVaultBalanceChange.token.symbol, mockOrderTradesList[0].inputVaultBalanceChange.vault.token.symbol ); assert.equal( - result[0].inputVaultBalanceChange.token.decimals, + result.trades[0].inputVaultBalanceChange.token.decimals, BigInt(mockOrderTradesList[0].inputVaultBalanceChange.vault.token.decimals ?? 0) ); - assert.equal(result[0].transaction.id, mockOrderTradesList[0].tradeEvent.transaction.id); + assert.equal(result.trades[0].transaction.id, mockOrderTradesList[0].tradeEvent.transaction.id); assert.equal( - result[0].transaction.from, + result.trades[0].transaction.from, mockOrderTradesList[0].tradeEvent.transaction.from ); assert.equal( - result[0].transaction.blockNumber, + result.trades[0].transaction.blockNumber, BigInt(mockOrderTradesList[0].tradeEvent.transaction.blockNumber) ); assert.equal( - result[0].transaction.timestamp, + result.trades[0].transaction.timestamp, BigInt(mockOrderTradesList[0].tradeEvent.transaction.timestamp) ); }); @@ -1189,40 +1191,7 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f assert.equal(result.orderbook, mockTrade.orderbook.id.toLowerCase()); }); - it('should get trade count', async function () { - await mockServer - .forPost('/sg1') - .once() - .thenReply(200, JSON.stringify({ data: { orders: [order1] } })); - await mockServer - .forPost('/sg1') - .once() - .thenReply(200, JSON.stringify({ data: { orders: [order1] } })); - await mockServer.forPost('/sg1').thenReply( - 200, - JSON.stringify({ - data: { - trades: mockOrderTradesList - } - }) - ); - await mockServer.forPost('/sg1').thenReply( - 200, - JSON.stringify({ - data: { - trades: [] - } - }) - ); - - const raindexClient = extractWasmEncodedData(await RaindexClient.new([YAML])); - const order = extractWasmEncodedData( - await raindexClient.getOrderByHash(1, CHAIN_ID_1_ORDERBOOK_ADDRESS, BYTES32_0123) - ); - const result = extractWasmEncodedData(await order.getTradeCount()); - assert.equal(result, 1); }); - }); }); describe('Add and remove orders', async function () { diff --git a/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts b/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts index 96c224e8dd..a8abe094f0 100644 --- a/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts +++ b/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts @@ -140,10 +140,15 @@ vi.mock('@tanstack/svelte-query'); const mockOrder: RaindexOrder = { id: '1', - getTradeCount: vi.fn(), getTradesList: vi.fn() } as unknown as RaindexOrder; +const wrapInResult = (trades: RaindexTrade[]) => ({ + trades, + totalCount: trades.length, + summary: {} +}); + test('renders table with correct data', async () => { const queryClient = new QueryClient(); @@ -153,7 +158,7 @@ test('renders table with correct data', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any subscribe: (fn: (value: any) => void) => { fn({ - data: { pages: [mockTradeOrdersList] }, + data: { pages: [wrapInResult(mockTradeOrdersList)] }, status: 'success', isFetching: false, isFetched: true @@ -192,7 +197,7 @@ test('renders a debug button for each trade', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any subscribe: (fn: (value: any) => void) => { fn({ - data: { pages: [mockTradeOrdersList] }, + data: { pages: [wrapInResult(mockTradeOrdersList)] }, status: 'success', isFetching: false, isFetched: true @@ -225,7 +230,7 @@ test('renders combined Transaction column with Sender and Tx', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any subscribe: (fn: (value: any) => void) => { fn({ - data: { pages: [mockTradeOrdersList] }, + data: { pages: [wrapInResult(mockTradeOrdersList)] }, status: 'success', isFetching: false, isFetched: true @@ -260,7 +265,7 @@ test('renders Input column with token symbol and amount', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any subscribe: (fn: (value: any) => void) => { fn({ - data: { pages: [mockTradeOrdersList] }, + data: { pages: [wrapInResult(mockTradeOrdersList)] }, status: 'success', isFetching: false, isFetched: true @@ -297,7 +302,7 @@ test('renders Output column with token symbol and amount', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any subscribe: (fn: (value: any) => void) => { fn({ - data: { pages: [mockTradeOrdersList] }, + data: { pages: [wrapInResult(mockTradeOrdersList)] }, status: 'success', isFetching: false, isFetched: true @@ -400,7 +405,7 @@ test('displays dash when output amount is zero (prevents division by zero)', asy mockQuery.createInfiniteQuery = vi.fn((__options, _queryClient) => ({ subscribe: (fn: (value: unknown) => void) => { fn({ - data: { pages: [mockTrades] }, + data: { pages: [wrapInResult(mockTrades)] }, status: 'success', isFetching: false, isFetched: true @@ -430,7 +435,7 @@ test('displays dash when input amount is zero', async () => { mockQuery.createInfiniteQuery = vi.fn((__options, _queryClient) => ({ subscribe: (fn: (value: unknown) => void) => { fn({ - data: { pages: [mockTrades] }, + data: { pages: [wrapInResult(mockTrades)] }, status: 'success', isFetching: false, isFetched: true @@ -460,7 +465,7 @@ test('displays dash when both amounts are zero', async () => { mockQuery.createInfiniteQuery = vi.fn((__options, _queryClient) => ({ subscribe: (fn: (value: unknown) => void) => { fn({ - data: { pages: [mockTrades] }, + data: { pages: [wrapInResult(mockTrades)] }, status: 'success', isFetching: false, isFetched: true diff --git a/packages/ui-components/src/lib/components/charts/OrderTradesChart.svelte b/packages/ui-components/src/lib/components/charts/OrderTradesChart.svelte index 983beace6a..5e1d89efe5 100644 --- a/packages/ui-components/src/lib/components/charts/OrderTradesChart.svelte +++ b/packages/ui-components/src/lib/components/charts/OrderTradesChart.svelte @@ -60,7 +60,7 @@ queryFn: async () => { const data = await order.getTradesList(undefined, undefined, 1); if (data.error) throw new Error(data.error.readableMsg); - return data.value; + return data.value.trades; } }); diff --git a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte index 799745108f..c45bc65ff6 100644 --- a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte @@ -7,7 +7,7 @@ import { formatTimestampSecondsAsLocal } from '../../services/time'; import Hash, { HashType } from '../Hash.svelte'; import { BugOutline } from 'flowbite-svelte-icons'; - import type { RaindexOrder, RaindexTrade } from '@rainlanguage/orderbook'; + import type { RaindexOrder, RaindexTrade, RaindexTradesListResult } from '@rainlanguage/orderbook'; import TableTimeFilters from '../charts/TableTimeFilters.svelte'; import Tooltip from '../Tooltip.svelte'; @@ -25,44 +25,33 @@ queryFn: async ({ pageParam }: { pageParam: number }) => { tradesCount = undefined; - const [countResult, tradesResult] = await Promise.all([ - order.getTradeCount( - startTimestamp ? BigInt(startTimestamp) : undefined, - endTimestamp ? BigInt(endTimestamp) : undefined - ), - order.getTradesList( - startTimestamp ? BigInt(startTimestamp) : undefined, - endTimestamp ? BigInt(endTimestamp) : undefined, - pageParam + 1 - ) - ]); - if (countResult.error) throw new Error(countResult.error.readableMsg); - if (tradesResult.error) throw new Error(tradesResult.error.readableMsg); + const result = await order.getTradesList( + startTimestamp ? BigInt(startTimestamp) : undefined, + endTimestamp ? BigInt(endTimestamp) : undefined, + pageParam + 1 + ); + if (result.error) throw new Error(result.error.readableMsg); - const count = countResult.value; - const trades = tradesResult.value; + tradesCount = result.value.totalCount; - if (typeof count === 'number') { - tradesCount = count; - } - - return trades; + return result.value; }, initialPageParam: 0, getNextPageParam: ( - lastPage: RaindexTrade[], - _allPages: RaindexTrade[][], + lastPage: RaindexTradesListResult, + _allPages: RaindexTradesListResult[], lastPageParam: number ) => { - return lastPage.length === DEFAULT_PAGE_SIZE ? lastPageParam + 1 : undefined; + return lastPage.trades.length === DEFAULT_PAGE_SIZE ? lastPageParam + 1 : undefined; } }); - const AppTable = TanstackAppTable; + const AppTable = TanstackAppTable; page.trades} emptyMessage="No trades found" rowHoverable={false} queryKey={order.id} From 9e87db89d18794d365dfaee98cfed7776400ff21 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 12:57:29 +0300 Subject: [PATCH 06/12] feat: fix trade pagination, update test fixtures and snapshots Fix pagination logic to check page size instead of empty results, update orderHash values to full 32-byte hex, add owner field to trade query snapshots, and fix orderbook filter key casing. --- crates/common/src/raindex_client/orders.rs | 3 ++- crates/common/src/raindex_client/trades.rs | 7 ++++--- crates/subgraph/src/orderbook_client/order_trade.rs | 12 +++++++----- .../order_trade_test__vaults_query_gql_output.snap | 1 + .../order_trades_test__vaults_query_gql_output.snap | 1 + 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/common/src/raindex_client/orders.rs b/crates/common/src/raindex_client/orders.rs index 5cb150ffd4..dc799fdd65 100644 --- a/crates/common/src/raindex_client/orders.rs +++ b/crates/common/src/raindex_client/orders.rs @@ -2989,7 +2989,8 @@ mod tests { }, "order": { "id": "0x557147dd0daa80d5beff0023fe6a3505469b2b8c4406ce1ab873e1a652572dd4", - "orderHash": "0x557147dd0daa80d5beff0023fe6a3505469b2b8c4406ce1ab873e1a652572dd4" + "orderHash": "0x557147dd0daa80d5beff0023fe6a3505469b2b8c4406ce1ab873e1a652572dd4", + "owner": "0xf08bcbce72f62c95dcb7c07dcb5ed26acfcfbc11" }, "orderbook": { "id": CHAIN_ID_1_ORDERBOOK_ADDRESS diff --git a/crates/common/src/raindex_client/trades.rs b/crates/common/src/raindex_client/trades.rs index 686c87000e..05e216ba22 100644 --- a/crates/common/src/raindex_client/trades.rs +++ b/crates/common/src/raindex_client/trades.rs @@ -4,8 +4,8 @@ use super::ClientRef; use super::*; use crate::local_db::query::fetch_order_trades::LocalDbOrderTrade; use crate::local_db::OrderbookIdentifier; +use crate::local_db::query::fetch_trades_by_tx::FetchTradesByTxArgs; use crate::raindex_client::{ - local_db::query::fetch_trades_by_tx::FetchTradesByTxArgs, orders::RaindexOrder, transactions::RaindexTransaction, vaults::{LocalTradeBalanceInfo, LocalTradeTokenInfo, RaindexVaultBalanceChange}, @@ -17,6 +17,7 @@ use rain_orderbook_subgraph_client::{ types::{common::SgTrade, Id}, MultiOrderbookSubgraphClient, }; +use std::ops::{Add, Div, Sub}; use std::str::FromStr; #[cfg(target_family = "wasm")] use wasm_bindgen_utils::prelude::js_sys::BigInt; @@ -1194,7 +1195,7 @@ mod test_helpers { }, "order": { "id": "0x0123", - "orderHash": "0x0123", + "orderHash": "0x0000000000000000000000000000000000000000000000000000000000000123", "owner": "0x0000000000000000000000000000000000000000" }, "inputVaultBalanceChange": { @@ -1285,7 +1286,7 @@ mod test_helpers { }, "order": { "id": "0x0234", - "orderHash": "0x0234", + "orderHash": "0x0000000000000000000000000000000000000000000000000000000000000234", "owner": "0x0000000000000000000000000000000000000001" }, "inputVaultBalanceChange": { diff --git a/crates/subgraph/src/orderbook_client/order_trade.rs b/crates/subgraph/src/orderbook_client/order_trade.rs index db47d715f1..01cf9857ec 100644 --- a/crates/subgraph/src/orderbook_client/order_trade.rs +++ b/crates/subgraph/src/orderbook_client/order_trade.rs @@ -69,10 +69,11 @@ impl OrderbookSubgraphClient { ) .await?; - if data.trades.is_empty() { + let page_len = data.trades.len(); + all_trades.extend(data.trades); + if page_len < ALL_PAGES_QUERY_PAGE_SIZE as usize { break; } - all_trades.extend(data.trades); page += 1; } Ok(all_trades) @@ -100,10 +101,11 @@ impl OrderbookSubgraphClient { end_timestamp, ) .await?; - if page_data.is_empty() { + let page_len = page_data.len(); + all_pages_merged.extend(page_data); + if page_len < ALL_PAGES_QUERY_PAGE_SIZE as usize { break; } - all_pages_merged.extend(page_data); page += 1 } Ok(all_pages_merged) @@ -559,7 +561,7 @@ mod tests { sg_server.mock(|when, then| { when.method(POST) .path("/") - .body_contains(format!("\"orderbook_in\":[\"{}\"]", orderbook)); + .body_contains(format!("\"orderbookIn\":[\"{}\"]", orderbook)); then.status(200).json_body(json!({"data": {"trades": []}})); }); diff --git a/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap index b3d715895f..58daa47d6b 100644 --- a/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap @@ -51,6 +51,7 @@ query SgOrderTradeDetailQuery($id: ID!) { order { id orderHash + owner } inputVaultBalanceChange { id diff --git a/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap index c2c056004d..32f43da126 100644 --- a/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap @@ -51,6 +51,7 @@ query SgOrderTradesListQuery($first: Int, $id: Bytes!, $skip: Int, $timestampGte order { id orderHash + owner } inputVaultBalanceChange { id From 9d0265df4caf81a38e516d0d329facd688d23d20 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 13:19:08 +0300 Subject: [PATCH 07/12] fix: update JS API test mocks to match new trade struct with owner field Add missing `owner` field to mock trade order objects, handle checksummed orderbook addresses, and compare Float amounts via asHex(). --- .../orderbook/test/js_api/raindexClient.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/orderbook/test/js_api/raindexClient.test.ts b/packages/orderbook/test/js_api/raindexClient.test.ts index 61f882a479..0e7ed13aaf 100644 --- a/packages/orderbook/test/js_api/raindexClient.test.ts +++ b/packages/orderbook/test/js_api/raindexClient.test.ts @@ -426,7 +426,8 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f }, order: { id: BYTES32_VOL_ORDER, - orderHash: BYTES32_VOL_ORDER + orderHash: BYTES32_VOL_ORDER, + owner: '0x0000000000000000000000000000000000000000' }, inputVaultBalanceChange: { amount: float50, @@ -503,7 +504,8 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f }, order: { id: order1.id, - orderHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + orderHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + owner: '0x0000000000000000000000000000000000000000' }, inputVaultBalanceChange: { amount: '0x0000000000000000000000000000000000000000000000000000000000000003', @@ -542,7 +544,8 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f id: BYTES32_0123, order: { id: BYTES32_0123, - orderHash: BYTES32_0123 + orderHash: BYTES32_0123, + owner: '0x0000000000000000000000000000000000000000' }, tradeEvent: { sender: '0x0000000000000000000000000000000000000000', @@ -1034,9 +1037,9 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f assert.equal(result.trades[0].id, mockOrderTradesList[0].id); assert.equal(result.trades[0].orderHash, mockOrderTradesList[0].order.orderHash); assert.equal(result.trades[0].timestamp, BigInt(mockOrderTradesList[0].timestamp)); - assert.equal(result.trades[0].orderbook, mockOrderTradesList[0].orderbook.id.toLowerCase()); + assert.equal(result.trades[0].orderbook.toLowerCase(), mockOrderTradesList[0].orderbook.id.toLowerCase()); assert.equal( - result.trades[0].outputVaultBalanceChange.amount, + result.trades[0].outputVaultBalanceChange.amount.asHex(), mockOrderTradesList[0].outputVaultBalanceChange.amount ); assert.equal( @@ -1064,7 +1067,7 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f BigInt(mockOrderTradesList[0].outputVaultBalanceChange.vault.token.decimals ?? 0) ); assert.equal( - result.trades[0].inputVaultBalanceChange.amount, + result.trades[0].inputVaultBalanceChange.amount.asHex(), mockOrderTradesList[0].inputVaultBalanceChange.amount ); assert.equal( From 345e65112375de55cf0cd31a5c79bcfe09d18760 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 13:38:56 +0300 Subject: [PATCH 08/12] fix: resolve formatting and bigint-to-number type error in OrderTradesListTable --- .../local_db/query/fetch_trades_by_tx/mod.rs | 16 ++++--------- crates/common/src/local_db/query/mod.rs | 2 +- .../local_db/query/fetch_trades_by_tx.rs | 3 +-- .../src/raindex_client/local_db/query/mod.rs | 2 +- crates/common/src/raindex_client/trades.rs | 23 ++++++++++--------- crates/subgraph/src/multi_orderbook_client.rs | 4 +++- .../src/orderbook_client/order_trade.rs | 9 ++++---- .../test/js_api/raindexClient.test.ts | 13 +++++++---- .../tables/OrderTradesListTable.svelte | 8 +++++-- 9 files changed, 41 insertions(+), 39 deletions(-) diff --git a/crates/common/src/local_db/query/fetch_trades_by_tx/mod.rs b/crates/common/src/local_db/query/fetch_trades_by_tx/mod.rs index 7fb56df619..bde5dc730a 100644 --- a/crates/common/src/local_db/query/fetch_trades_by_tx/mod.rs +++ b/crates/common/src/local_db/query/fetch_trades_by_tx/mod.rs @@ -71,8 +71,7 @@ mod tests { #[test] fn builds_with_chain_ids_and_tx_hash() { - let tx_hash = - b256!("0x00000000000000000000000000000000000000000000000000000000deadbeef"); + let tx_hash = b256!("0x00000000000000000000000000000000000000000000000000000000deadbeef"); let stmt = build_fetch_trades_by_tx_stmt(&FetchTradesByTxArgs { chain_ids: vec![137, 1, 137], orderbook_addresses: vec![], @@ -96,8 +95,7 @@ mod tests { #[test] fn builds_with_orderbook_address_filters() { - let tx_hash = - b256!("0x00000000000000000000000000000000000000000000000000000000deadbeef"); + let tx_hash = b256!("0x00000000000000000000000000000000000000000000000000000000deadbeef"); let ob = address!("0x2f209e5b67a33b8fe96e28f24628df6da301c8eb"); let stmt = build_fetch_trades_by_tx_stmt(&FetchTradesByTxArgs { chain_ids: vec![137], @@ -112,14 +110,8 @@ mod tests { ); assert_eq!(stmt.params[1], SqlValue::U64(137)); assert_eq!(stmt.params[2], SqlValue::U64(137)); - assert_eq!( - stmt.params[3], - SqlValue::Text(hex::encode_prefixed(ob)) - ); - assert_eq!( - stmt.params[4], - SqlValue::Text(hex::encode_prefixed(ob)) - ); + assert_eq!(stmt.params[3], SqlValue::Text(hex::encode_prefixed(ob))); + assert_eq!(stmt.params[4], SqlValue::Text(hex::encode_prefixed(ob))); assert!(stmt.sql.contains("t.orderbook_address IN (?4)")); assert!(stmt.sql.contains("c.orderbook_address IN (?5)")); assert!(!stmt.sql.contains(TAKE_ORDERS_ORDERBOOKS_CLAUSE)); diff --git a/crates/common/src/local_db/query/mod.rs b/crates/common/src/local_db/query/mod.rs index 6d53db9365..2a8e475c4d 100644 --- a/crates/common/src/local_db/query/mod.rs +++ b/crates/common/src/local_db/query/mod.rs @@ -9,7 +9,6 @@ pub mod fetch_erc20_tokens_by_addresses; pub mod fetch_last_synced_block; pub mod fetch_order_trades; pub mod fetch_order_trades_count; -pub mod fetch_trades_by_tx; pub mod fetch_order_vaults_volume; pub mod fetch_orders; pub(crate) mod fetch_orders_common; @@ -17,6 +16,7 @@ pub mod fetch_orders_count; pub mod fetch_store_addresses; pub mod fetch_tables; pub mod fetch_target_watermark; +pub mod fetch_trades_by_tx; pub mod fetch_transaction_by_hash; pub mod fetch_vault_balance_changes; pub mod fetch_vaults; diff --git a/crates/common/src/raindex_client/local_db/query/fetch_trades_by_tx.rs b/crates/common/src/raindex_client/local_db/query/fetch_trades_by_tx.rs index c5567d928b..f060c3fb03 100644 --- a/crates/common/src/raindex_client/local_db/query/fetch_trades_by_tx.rs +++ b/crates/common/src/raindex_client/local_db/query/fetch_trades_by_tx.rs @@ -25,8 +25,7 @@ mod wasm_tests { #[wasm_bindgen_test] async fn wrapper_uses_builder_sql_exactly() { - let tx_hash = - b256!("0x00000000000000000000000000000000000000000000000000000000deadbeef"); + let tx_hash = b256!("0x00000000000000000000000000000000000000000000000000000000deadbeef"); let args = FetchTradesByTxArgs { chain_ids: vec![137, 42161], orderbook_addresses: vec![], diff --git a/crates/common/src/raindex_client/local_db/query/mod.rs b/crates/common/src/raindex_client/local_db/query/mod.rs index 43fad7d2c5..0f69a05f69 100644 --- a/crates/common/src/raindex_client/local_db/query/mod.rs +++ b/crates/common/src/raindex_client/local_db/query/mod.rs @@ -5,12 +5,12 @@ pub mod fetch_erc20_tokens_by_addresses; pub mod fetch_last_synced_block; pub mod fetch_order_trades; pub mod fetch_order_trades_count; -pub mod fetch_trades_by_tx; pub mod fetch_order_vaults_volume; pub mod fetch_orders; pub mod fetch_orders_count; pub mod fetch_store_addresses; pub mod fetch_tables; +pub mod fetch_trades_by_tx; pub mod fetch_transaction_by_hash; pub mod fetch_vault_balance_changes; pub mod fetch_vaults; diff --git a/crates/common/src/raindex_client/trades.rs b/crates/common/src/raindex_client/trades.rs index 05e216ba22..0f691a745c 100644 --- a/crates/common/src/raindex_client/trades.rs +++ b/crates/common/src/raindex_client/trades.rs @@ -3,14 +3,14 @@ use super::orders::{OrdersDataSource, SubgraphOrders}; use super::ClientRef; use super::*; use crate::local_db::query::fetch_order_trades::LocalDbOrderTrade; -use crate::local_db::OrderbookIdentifier; use crate::local_db::query::fetch_trades_by_tx::FetchTradesByTxArgs; +use crate::local_db::OrderbookIdentifier; +use crate::raindex_client::local_db::query::fetch_trades_by_tx::fetch_trades_by_tx; use crate::raindex_client::{ orders::RaindexOrder, transactions::RaindexTransaction, vaults::{LocalTradeBalanceInfo, LocalTradeTokenInfo, RaindexVaultBalanceChange}, }; -use crate::raindex_client::local_db::query::fetch_trades_by_tx::fetch_trades_by_tx; use alloy::primitives::{Address, Bytes, B256, U256}; use rain_math_float::Float; use rain_orderbook_subgraph_client::{ @@ -182,7 +182,6 @@ impl RaindexClient { self.get_trades_for_transaction(chain_ids, orderbook_addresses, tx_hash) .await } - } impl RaindexClient { pub async fn get_trades_for_transaction( @@ -246,8 +245,7 @@ impl RaindexClient { trade_with_name.subgraph_name.clone(), trade_with_name.trade.id.0.clone(), ))?; - let trade = - RaindexTrade::try_from_sg_trade(chain_id, trade_with_name.trade)?; + let trade = RaindexTrade::try_from_sg_trade(chain_id, trade_with_name.trade)?; all_trades.push(trade); } } @@ -573,8 +571,7 @@ impl RaindexTradeSummary { for trade in trades { total_input = total_input.add(trade.input_vault_balance_change.amount())?; - let neg_output = - Float::zero()?.sub(trade.output_vault_balance_change.amount())?; + let neg_output = Float::zero()?.sub(trade.output_vault_balance_change.amount())?; total_output = total_output.add(neg_output)?; } @@ -1013,7 +1010,6 @@ mod test_helpers { fixture.output_token.to_string().to_lowercase() ); } - } #[cfg(not(target_family = "wasm"))] @@ -1524,7 +1520,10 @@ mod test_helpers { ); assert_eq!( trade1.order_hash(), - B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000123").unwrap() + B256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000123" + ) + .unwrap() ); let trade2 = trades[1].clone(); @@ -1705,9 +1704,11 @@ mod test_helpers { ); assert_eq!( trade.order_hash(), - B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000123").unwrap() + B256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000123" + ) + .unwrap() ); } - } } diff --git a/crates/subgraph/src/multi_orderbook_client.rs b/crates/subgraph/src/multi_orderbook_client.rs index 8c1b8ab33f..f1d2a0dadd 100644 --- a/crates/subgraph/src/multi_orderbook_client.rs +++ b/crates/subgraph/src/multi_orderbook_client.rs @@ -881,7 +881,9 @@ mod tests { #[tokio::test] async fn test_trades_by_transaction_no_subgraphs() { let client = MultiOrderbookSubgraphClient::new(vec![]); - let result = client.trades_by_transaction("0xtx123".to_string(), None).await; + let result = client + .trades_by_transaction("0xtx123".to_string(), None) + .await; assert!(result.is_empty()); } diff --git a/crates/subgraph/src/orderbook_client/order_trade.rs b/crates/subgraph/src/orderbook_client/order_trade.rs index 01cf9857ec..33cbed6bc0 100644 --- a/crates/subgraph/src/orderbook_client/order_trade.rs +++ b/crates/subgraph/src/orderbook_client/order_trade.rs @@ -53,11 +53,10 @@ impl OrderbookSubgraphClient { let mut page = 1; loop { - let pagination_variables = - Self::parse_pagination_args(SgPaginationArgs { - page, - page_size: ALL_PAGES_QUERY_PAGE_SIZE, - }); + let pagination_variables = Self::parse_pagination_args(SgPaginationArgs { + page, + page_size: ALL_PAGES_QUERY_PAGE_SIZE, + }); let data = self .query::( SgPaginationWithTxIdQueryVariables { diff --git a/packages/orderbook/test/js_api/raindexClient.test.ts b/packages/orderbook/test/js_api/raindexClient.test.ts index 0e7ed13aaf..e7dba6a238 100644 --- a/packages/orderbook/test/js_api/raindexClient.test.ts +++ b/packages/orderbook/test/js_api/raindexClient.test.ts @@ -1037,7 +1037,10 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f assert.equal(result.trades[0].id, mockOrderTradesList[0].id); assert.equal(result.trades[0].orderHash, mockOrderTradesList[0].order.orderHash); assert.equal(result.trades[0].timestamp, BigInt(mockOrderTradesList[0].timestamp)); - assert.equal(result.trades[0].orderbook.toLowerCase(), mockOrderTradesList[0].orderbook.id.toLowerCase()); + assert.equal( + result.trades[0].orderbook.toLowerCase(), + mockOrderTradesList[0].orderbook.id.toLowerCase() + ); assert.equal( result.trades[0].outputVaultBalanceChange.amount.asHex(), mockOrderTradesList[0].outputVaultBalanceChange.amount @@ -1094,7 +1097,10 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f result.trades[0].inputVaultBalanceChange.token.decimals, BigInt(mockOrderTradesList[0].inputVaultBalanceChange.vault.token.decimals ?? 0) ); - assert.equal(result.trades[0].transaction.id, mockOrderTradesList[0].tradeEvent.transaction.id); + assert.equal( + result.trades[0].transaction.id, + mockOrderTradesList[0].tradeEvent.transaction.id + ); assert.equal( result.trades[0].transaction.from, mockOrderTradesList[0].tradeEvent.transaction.from @@ -1193,8 +1199,7 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f ); assert.equal(result.orderbook, mockTrade.orderbook.id.toLowerCase()); }); - - }); + }); }); describe('Add and remove orders', async function () { diff --git a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte index c45bc65ff6..89b53c6104 100644 --- a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte @@ -7,7 +7,11 @@ import { formatTimestampSecondsAsLocal } from '../../services/time'; import Hash, { HashType } from '../Hash.svelte'; import { BugOutline } from 'flowbite-svelte-icons'; - import type { RaindexOrder, RaindexTrade, RaindexTradesListResult } from '@rainlanguage/orderbook'; + import type { + RaindexOrder, + RaindexTrade, + RaindexTradesListResult + } from '@rainlanguage/orderbook'; import TableTimeFilters from '../charts/TableTimeFilters.svelte'; import Tooltip from '../Tooltip.svelte'; @@ -32,7 +36,7 @@ ); if (result.error) throw new Error(result.error.readableMsg); - tradesCount = result.value.totalCount; + tradesCount = Number(result.value.totalCount); return result.value; }, From a27b35ce8de0a1ec297a55f8a4efaa75a8985a4f Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 15:11:17 +0300 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94?= =?UTF-8?q?=20pair-bucketed=20summary,=20BigInt=20mock,=20chart=20paginati?= =?UTF-8?q?on,=20totalCount=20stop=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/common/src/raindex_client/trades.rs | 143 +++++++++++++----- .../__tests__/OrderTradesListTable.test.ts | 2 +- .../components/charts/OrderTradesChart.svelte | 2 +- .../tables/OrderTradesListTable.svelte | 6 +- 4 files changed, 110 insertions(+), 43 deletions(-) diff --git a/crates/common/src/raindex_client/trades.rs b/crates/common/src/raindex_client/trades.rs index 0f691a745c..10f6ee5c72 100644 --- a/crates/common/src/raindex_client/trades.rs +++ b/crates/common/src/raindex_client/trades.rs @@ -147,7 +147,7 @@ impl RaindexClient { /// ``` #[wasm_export( js_name = "getTradesForTransaction", - return_description = "Trades list result with total count and summary", + return_description = "Trades list result with total count and per-pair summary", unchecked_return_type = "RaindexTradesListResult", preserve_js_class )] @@ -252,12 +252,12 @@ impl RaindexClient { } let total_count = all_trades.len() as u64; - let summary = RaindexTradeSummary::from_trades(&all_trades)?; + let summary = RaindexPairSummary::from_trades(&all_trades)?; Ok(RaindexTradesListResult { trades: all_trades, total_count, - summary, + summary: Some(summary), }) } } @@ -267,7 +267,7 @@ impl RaindexOrder { /// Fetches trade history with optional time filtering /// /// Retrieves a chronological list of trades executed by an order within - /// an optional time range, along with the total count and summary. + /// an optional time range, along with the total count and optional per-pair summary. /// /// ## Examples /// @@ -281,7 +281,7 @@ impl RaindexOrder { /// ``` #[wasm_export( js_name = "getTradesList", - return_description = "Trades list result with total count and summary", + return_description = "Trades list result with total count and optional per-pair summary", unchecked_return_type = "RaindexTradesListResult", preserve_js_class )] @@ -348,7 +348,11 @@ impl RaindexOrder { } }; - let summary = RaindexTradeSummary::from_trades(&trades)?; + let summary = if page.is_some() { + None + } else { + Some(RaindexPairSummary::from_trades(&trades)?) + }; Ok(RaindexTradesListResult { trades, @@ -504,18 +508,34 @@ impl RaindexTrade { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[wasm_bindgen] -pub struct RaindexTradeSummary { +pub struct RaindexPairSummary { + chain_id: u32, + input_token: Address, + output_token: Address, total_input: Float, formatted_total_input: String, total_output: Float, formatted_total_output: String, average_io_ratio: Float, formatted_average_io_ratio: String, + trade_count: u64, } #[cfg(target_family = "wasm")] #[wasm_bindgen] -impl RaindexTradeSummary { +impl RaindexPairSummary { + #[wasm_bindgen(getter = chainId)] + pub fn chain_id(&self) -> u32 { + self.chain_id + } + #[wasm_bindgen(getter = inputToken, unchecked_return_type = "Address")] + pub fn input_token(&self) -> String { + self.input_token.to_string() + } + #[wasm_bindgen(getter = outputToken, unchecked_return_type = "Address")] + pub fn output_token(&self) -> String { + self.output_token.to_string() + } #[wasm_bindgen(getter = totalInput)] pub fn total_input(&self) -> Float { self.total_input @@ -540,10 +560,23 @@ impl RaindexTradeSummary { pub fn formatted_average_io_ratio(&self) -> String { self.formatted_average_io_ratio.clone() } + #[wasm_bindgen(getter = tradeCount)] + pub fn trade_count(&self) -> u64 { + self.trade_count + } } #[cfg(not(target_family = "wasm"))] -impl RaindexTradeSummary { +impl RaindexPairSummary { + pub fn chain_id(&self) -> u32 { + self.chain_id + } + pub fn input_token(&self) -> Address { + self.input_token + } + pub fn output_token(&self) -> Address { + self.output_token + } pub fn total_input(&self) -> Float { self.total_input } @@ -562,37 +595,71 @@ impl RaindexTradeSummary { pub fn formatted_average_io_ratio(&self) -> &str { &self.formatted_average_io_ratio } + pub fn trade_count(&self) -> u64 { + self.trade_count + } } -impl RaindexTradeSummary { - pub fn from_trades(trades: &[RaindexTrade]) -> Result { - let mut total_input = Float::zero()?; - let mut total_output = Float::zero()?; +impl RaindexPairSummary { + pub fn from_trades(trades: &[RaindexTrade]) -> Result, RaindexError> { + use std::collections::HashMap; + + let mut buckets: HashMap<(u32, Address, Address), Vec<&RaindexTrade>> = HashMap::new(); for trade in trades { - total_input = total_input.add(trade.input_vault_balance_change.amount())?; - let neg_output = Float::zero()?.sub(trade.output_vault_balance_change.amount())?; - total_output = total_output.add(neg_output)?; + #[cfg(target_family = "wasm")] + let input_token = + Address::from_str(&trade.input_vault_balance_change.token().address())?; + #[cfg(not(target_family = "wasm"))] + let input_token = trade.input_vault_balance_change.token().address(); + + #[cfg(target_family = "wasm")] + let output_token = + Address::from_str(&trade.output_vault_balance_change.token().address())?; + #[cfg(not(target_family = "wasm"))] + let output_token = trade.output_vault_balance_change.token().address(); + + let key = (trade.chain_id, input_token, output_token); + buckets.entry(key).or_default().push(trade); } - let formatted_total_input = total_input.format()?; - let formatted_total_output = total_output.format()?; + let mut summaries = Vec::with_capacity(buckets.len()); - let average_io_ratio = if total_output.eq(Float::zero()?).unwrap_or(true) { - Float::zero()? - } else { - total_input.div(total_output)? - }; - let formatted_average_io_ratio = average_io_ratio.format()?; - - Ok(RaindexTradeSummary { - total_input, - formatted_total_input, - total_output, - formatted_total_output, - average_io_ratio, - formatted_average_io_ratio, - }) + for ((chain_id, input_token, output_token), bucket) in buckets { + let mut total_input = Float::zero()?; + let mut total_output = Float::zero()?; + + for trade in &bucket { + total_input = total_input.add(trade.input_vault_balance_change.amount())?; + let neg_output = Float::zero()?.sub(trade.output_vault_balance_change.amount())?; + total_output = total_output.add(neg_output)?; + } + + let formatted_total_input = total_input.format()?; + let formatted_total_output = total_output.format()?; + + let average_io_ratio = if total_output.eq(Float::zero()?).unwrap_or(true) { + Float::zero()? + } else { + total_input.div(total_output)? + }; + let formatted_average_io_ratio = average_io_ratio.format()?; + + summaries.push(RaindexPairSummary { + chain_id, + input_token, + output_token, + total_input, + formatted_total_input, + total_output, + formatted_total_output, + average_io_ratio, + formatted_average_io_ratio, + trade_count: bucket.len() as u64, + }); + } + + Ok(summaries) } } @@ -602,7 +669,7 @@ impl RaindexTradeSummary { pub struct RaindexTradesListResult { trades: Vec, total_count: u64, - summary: RaindexTradeSummary, + summary: Option>, } #[cfg(target_family = "wasm")] @@ -616,8 +683,8 @@ impl RaindexTradesListResult { pub fn total_count(&self) -> u64 { self.total_count } - #[wasm_bindgen(getter)] - pub fn summary(&self) -> RaindexTradeSummary { + #[wasm_bindgen(getter, unchecked_return_type = "RaindexPairSummary[] | undefined")] + pub fn summary(&self) -> Option> { self.summary.clone() } } @@ -630,8 +697,8 @@ impl RaindexTradesListResult { pub fn total_count(&self) -> u64 { self.total_count } - pub fn summary(&self) -> &RaindexTradeSummary { - &self.summary + pub fn summary(&self) -> Option<&[RaindexPairSummary]> { + self.summary.as_deref() } } diff --git a/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts b/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts index a8abe094f0..7e303d42a7 100644 --- a/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts +++ b/packages/ui-components/src/__tests__/OrderTradesListTable.test.ts @@ -145,7 +145,7 @@ const mockOrder: RaindexOrder = { const wrapInResult = (trades: RaindexTrade[]) => ({ trades, - totalCount: trades.length, + totalCount: BigInt(trades.length), summary: {} }); diff --git a/packages/ui-components/src/lib/components/charts/OrderTradesChart.svelte b/packages/ui-components/src/lib/components/charts/OrderTradesChart.svelte index 5e1d89efe5..62065fbb75 100644 --- a/packages/ui-components/src/lib/components/charts/OrderTradesChart.svelte +++ b/packages/ui-components/src/lib/components/charts/OrderTradesChart.svelte @@ -58,7 +58,7 @@ $: query = createQuery({ queryKey: [QKEY_ORDER_TRADES_LIST, order.id], queryFn: async () => { - const data = await order.getTradesList(undefined, undefined, 1); + const data = await order.getTradesList(undefined, undefined, undefined); if (data.error) throw new Error(data.error.readableMsg); return data.value.trades; } diff --git a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte index 89b53c6104..0838678a37 100644 --- a/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderTradesListTable.svelte @@ -2,7 +2,6 @@ import { createInfiniteQuery } from '@tanstack/svelte-query'; import TanstackAppTable from '../TanstackAppTable.svelte'; import { QKEY_ORDER_TRADES_LIST } from '../../queries/keys'; - import { DEFAULT_PAGE_SIZE } from '../../queries/constants'; import { TableBodyCell, TableHeadCell } from 'flowbite-svelte'; import { formatTimestampSecondsAsLocal } from '../../services/time'; import Hash, { HashType } from '../Hash.svelte'; @@ -43,10 +42,11 @@ initialPageParam: 0, getNextPageParam: ( lastPage: RaindexTradesListResult, - _allPages: RaindexTradesListResult[], + allPages: RaindexTradesListResult[], lastPageParam: number ) => { - return lastPage.trades.length === DEFAULT_PAGE_SIZE ? lastPageParam + 1 : undefined; + const fetchedCount = allPages.reduce((sum, p) => sum + p.trades.length, 0); + return fetchedCount < Number(lastPage.totalCount) ? lastPageParam + 1 : undefined; } }); From 9ec5221e6bec4908058ae4b3889c4c6523f83b98 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 9 Mar 2026 16:37:23 +0300 Subject: [PATCH 10/12] refactor: extract compute_io_ratio helper and hoist Float::zero out of loop --- crates/common/src/raindex_client/trades.rs | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/common/src/raindex_client/trades.rs b/crates/common/src/raindex_client/trades.rs index 10f6ee5c72..83cfb4de59 100644 --- a/crates/common/src/raindex_client/trades.rs +++ b/crates/common/src/raindex_client/trades.rs @@ -407,6 +407,16 @@ impl RaindexOrder { } impl RaindexTrade { + fn compute_io_ratio( + input_amount: Float, + output_amount: Float, + ) -> Result<(Float, String), RaindexError> { + let neg_output = Float::zero()?.sub(output_amount)?; + let io_ratio = input_amount.div(neg_output)?; + let formatted_io_ratio = io_ratio.format()?; + Ok((io_ratio, formatted_io_ratio)) + } + pub fn try_from_sg_trade(chain_id: u32, trade: SgTrade) -> Result { let input_vault_balance_change = RaindexVaultBalanceChange::try_from_sg_trade_balance_change( @@ -419,9 +429,10 @@ impl RaindexTrade { trade.output_vault_balance_change, )?; - let neg_output = Float::zero()?.sub(output_vault_balance_change.amount())?; - let io_ratio = input_vault_balance_change.amount().div(neg_output)?; - let formatted_io_ratio = io_ratio.format()?; + let (io_ratio, formatted_io_ratio) = Self::compute_io_ratio( + input_vault_balance_change.amount(), + output_vault_balance_change.amount(), + )?; Ok(RaindexTrade { id: Bytes::from_str(&trade.id.0)?, @@ -485,9 +496,8 @@ impl RaindexTrade { trade.block_timestamp, )?; - let neg_output = Float::zero()?.sub(output_change.amount())?; - let io_ratio = input_change.amount().div(neg_output)?; - let formatted_io_ratio = io_ratio.format()?; + let (io_ratio, formatted_io_ratio) = + Self::compute_io_ratio(input_change.amount(), output_change.amount())?; Ok(RaindexTrade { id: Bytes::from_str(&trade.trade_id)?, @@ -629,9 +639,10 @@ impl RaindexPairSummary { let mut total_input = Float::zero()?; let mut total_output = Float::zero()?; + let zero = Float::zero()?; for trade in &bucket { total_input = total_input.add(trade.input_vault_balance_change.amount())?; - let neg_output = Float::zero()?.sub(trade.output_vault_balance_change.amount())?; + let neg_output = zero.sub(trade.output_vault_balance_change.amount())?; total_output = total_output.add(neg_output)?; } From 6ac959466bcd70df8e1c1bb010d38d4640c4f16b Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Tue, 10 Mar 2026 09:27:17 +0300 Subject: [PATCH 11/12] fix: sort pair summaries deterministically, add query variable assertions to trade tests --- crates/common/src/raindex_client/trades.rs | 8 ++ crates/subgraph/src/multi_orderbook_client.rs | 84 ++++++++++++++++--- 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/crates/common/src/raindex_client/trades.rs b/crates/common/src/raindex_client/trades.rs index 83cfb4de59..5590211bd0 100644 --- a/crates/common/src/raindex_client/trades.rs +++ b/crates/common/src/raindex_client/trades.rs @@ -670,6 +670,14 @@ impl RaindexPairSummary { }); } + summaries.sort_by(|a, b| { + (a.chain_id, a.input_token, a.output_token).cmp(&( + b.chain_id, + b.input_token, + b.output_token, + )) + }); + Ok(summaries) } } diff --git a/crates/subgraph/src/multi_orderbook_client.rs b/crates/subgraph/src/multi_orderbook_client.rs index f1d2a0dadd..adf3c2f6ef 100644 --- a/crates/subgraph/src/multi_orderbook_client.rs +++ b/crates/subgraph/src/multi_orderbook_client.rs @@ -894,16 +894,21 @@ mod tests { let server1 = MockServer::start_async().await; let sg1_url = Url::parse(&server1.url("")).unwrap(); let sg1_name = "subgraph_alpha"; + let tx_id = "0xtx_abc"; let trade1 = default_sg_trade(); server1.mock(|when, then| { - when.method(POST).path("/").body_contains("\"skip\":0"); + when.method(POST) + .path("/") + .body_contains(tx_id) + .body_contains("\"skip\":0"); then.status(200) .json_body(json!({"data": {"trades": [trade1]}})); }); server1.mock(|when, then| { when.method(POST) .path("/") + .body_contains(tx_id) .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); then.status(200).json_body(json!({"data": {"trades": []}})); }); @@ -914,7 +919,49 @@ mod tests { }]); let trades = client - .trades_by_transaction("0xtx_abc".to_string(), None) + .trades_by_transaction(tx_id.to_string(), None) + .await; + assert_eq!(trades.len(), 1); + assert_eq!(trades[0].trade.id, trade1.id); + assert_eq!(trades[0].subgraph_name, sg1_name); + } + + #[tokio::test] + async fn test_trades_by_transaction_with_orderbook_filter() { + use crate::orderbook_client::ALL_PAGES_QUERY_PAGE_SIZE; + + let server1 = MockServer::start_async().await; + let sg1_url = Url::parse(&server1.url("")).unwrap(); + let sg1_name = "subgraph_ob_filter"; + let tx_id = "0xtx_ob_filter"; + let orderbook_addr = "0x1234567890abcdef1234567890abcdef12345678"; + + let trade1 = default_sg_trade(); + server1.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(tx_id) + .body_contains(orderbook_addr) + .body_contains("\"skip\":0"); + then.status(200) + .json_body(json!({"data": {"trades": [trade1]}})); + }); + server1.mock(|when, then| { + when.method(POST) + .path("/") + .body_contains(tx_id) + .body_contains(orderbook_addr) + .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); + then.status(200).json_body(json!({"data": {"trades": []}})); + }); + + let client = MultiOrderbookSubgraphClient::new(vec![MultiSubgraphArgs { + url: sg1_url, + name: sg1_name.to_string(), + }]); + + let trades = client + .trades_by_transaction(tx_id.to_string(), Some(vec![orderbook_addr.to_string()])) .await; assert_eq!(trades.len(), 1); assert_eq!(trades[0].trade.id, trade1.id); @@ -932,29 +979,38 @@ mod tests { let server2 = MockServer::start_async().await; let sg2_url = Url::parse(&server2.url("")).unwrap(); let sg2_name = "sg_two"; + let tx_id = "0xtx_multi"; let trade_s1 = default_sg_trade(); let trade_s2 = default_sg_trade(); server1.mock(|when, then| { - when.method(POST).path("/").body_contains("\"skip\":0"); + when.method(POST) + .path("/") + .body_contains(tx_id) + .body_contains("\"skip\":0"); then.status(200) .json_body(json!({"data": {"trades": [trade_s1]}})); }); server1.mock(|when, then| { when.method(POST) .path("/") + .body_contains(tx_id) .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); then.status(200).json_body(json!({"data": {"trades": []}})); }); server2.mock(|when, then| { - when.method(POST).path("/").body_contains("\"skip\":0"); + when.method(POST) + .path("/") + .body_contains(tx_id) + .body_contains("\"skip\":0"); then.status(200) .json_body(json!({"data": {"trades": [trade_s2]}})); }); server2.mock(|when, then| { when.method(POST) .path("/") + .body_contains(tx_id) .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); then.status(200).json_body(json!({"data": {"trades": []}})); }); @@ -971,7 +1027,7 @@ mod tests { ]); let trades = client - .trades_by_transaction("0xtx_multi".to_string(), None) + .trades_by_transaction(tx_id.to_string(), None) .await; assert_eq!(trades.len(), 2); @@ -992,21 +1048,26 @@ mod tests { let server2 = MockServer::start_async().await; let sg2_url = Url::parse(&server2.url("")).unwrap(); let sg2_name = "sg_two_error"; + let tx_id = "0xtx_partial"; let trade_s1 = default_sg_trade(); server1.mock(|when, then| { - when.method(POST).path("/").body_contains("\"skip\":0"); + when.method(POST) + .path("/") + .body_contains(tx_id) + .body_contains("\"skip\":0"); then.status(200) .json_body(json!({"data": {"trades": [trade_s1]}})); }); server1.mock(|when, then| { when.method(POST) .path("/") + .body_contains(tx_id) .body_contains(format!("\"skip\":{}", ALL_PAGES_QUERY_PAGE_SIZE)); then.status(200).json_body(json!({"data": {"trades": []}})); }); server2.mock(|when, then| { - when.method(POST).path("/"); + when.method(POST).path("/").body_contains(tx_id); then.status(500); }); @@ -1021,7 +1082,7 @@ mod tests { }, ]); let trades = client - .trades_by_transaction("0xtx_partial".to_string(), None) + .trades_by_transaction(tx_id.to_string(), None) .await; assert_eq!(trades.len(), 1); assert_eq!(trades[0].trade.id, trade_s1.id); @@ -1037,13 +1098,14 @@ mod tests { let server2 = MockServer::start_async().await; let sg2_url = Url::parse(&server2.url("")).unwrap(); let sg2_name = "sg_two_err"; + let tx_id = "0xtx_all_err"; server1.mock(|when, then| { - when.method(POST).path("/"); + when.method(POST).path("/").body_contains(tx_id); then.status(500); }); server2.mock(|when, then| { - when.method(POST).path("/"); + when.method(POST).path("/").body_contains(tx_id); then.status(500); }); @@ -1058,7 +1120,7 @@ mod tests { }, ]); let trades = client - .trades_by_transaction("0xtx_all_err".to_string(), None) + .trades_by_transaction(tx_id.to_string(), None) .await; assert!(trades.is_empty()); } From 384ece0029901be76cd883a65713af60fff1c497 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Mon, 6 Apr 2026 12:07:25 +0300 Subject: [PATCH 12/12] Add orderbook format checks --- flake.nix | 1 + package.json | 3 ++- packages/orderbook/package.json | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index d40d887503..323cda21b7 100644 --- a/flake.nix +++ b/flake.nix @@ -148,6 +148,7 @@ set -euxo pipefail cd packages/orderbook npm install --no-check + npm run format-check npm run build npm test ''; diff --git a/package.json b/package.json index 0d0f07bcc9..4f0d7d4091 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,9 @@ "test": "npm run test --workspaces", "lint:all": "npm run lint --workspaces --if-present", "format:all": "npm run format --workspaces --if-present", + "format-check:all": "npm run format-check --workspaces --if-present", "check:all": "npm run check --workspaces --if-present", - "lint-format-check:all": "npm run lint:all && npm run format:all && npm run check:all" + "lint-format-check:all": "npm run lint:all && npm run format-check:all && npm run check:all" }, "devDependencies": { "@square/svelte-store": "1.0.18", diff --git a/packages/orderbook/package.json b/packages/orderbook/package.json index d5dd1736d5..2b27874964 100644 --- a/packages/orderbook/package.json +++ b/packages/orderbook/package.json @@ -37,6 +37,7 @@ "rm-temp": "rimraf ./temp", "test": "npm run check && vitest run --dir test", "format": "prettier --write test", + "format-check": "prettier --list-different test", "check": "tsc ./dist/**/*.{ts,js} --noEmit --allowJs --lib es2020,dom", "docs": "typedoc", "docs:clean": "rimraf ./docs"