diff --git a/crates/common/src/local_db/query/fetch_trades/mod.rs b/crates/common/src/local_db/query/fetch_trades/mod.rs index eb16983de4..7a33d4546d 100644 --- a/crates/common/src/local_db/query/fetch_trades/mod.rs +++ b/crates/common/src/local_db/query/fetch_trades/mod.rs @@ -19,10 +19,21 @@ const CLEAR_EVENTS_ORDERBOOKS_CLAUSE: &str = "/*CLEAR_EVENTS_ORDERBOOKS_CLAUSE*/ const CLEAR_EVENTS_ORDERBOOKS_CLAUSE_BODY: &str = "AND c.orderbook_address IN ({list})"; const CLEAR_EVENTS_TAKERS_CLAUSE: &str = "/*CLEAR_EVENTS_TAKERS_CLAUSE*/"; const CLEAR_EVENTS_TAKERS_CLAUSE_BODY: &str = "AND c.sender IN ({list})"; +const CLEAR_EVENTS_ORDER_HASHES_CLAUSE: &str = "/*CLEAR_EVENTS_ORDER_HASHES_CLAUSE*/"; +const CLEAR_EVENTS_ORDER_HASHES_CLAUSE_BODY: &str = + "AND (c.alice_order_hash IN ({list}) OR c.bob_order_hash IN ({list}))"; +const TAKE_TRADES_ORDER_HASHES_CLAUSE: &str = "/*TAKE_TRADES_ORDER_HASHES_CLAUSE*/"; +const TAKE_TRADES_ORDER_HASHES_CLAUSE_BODY: &str = "AND oe.order_hash IN ({list})"; +const CLEAR_ALICE_ORDER_HASHES_CLAUSE: &str = "/*CLEAR_ALICE_ORDER_HASHES_CLAUSE*/"; +const CLEAR_ALICE_ORDER_HASHES_CLAUSE_BODY: &str = "AND mc.alice_order_hash IN ({list})"; +const CLEAR_BOB_ORDER_HASHES_CLAUSE: &str = "/*CLEAR_BOB_ORDER_HASHES_CLAUSE*/"; +const CLEAR_BOB_ORDER_HASHES_CLAUSE_BODY: &str = "AND mc.bob_order_hash IN ({list})"; const OWNERS_CLAUSE: &str = "/*OWNERS_CLAUSE*/"; const OWNERS_CLAUSE_BODY: &str = "AND tws.order_owner IN ({list})"; const ORDER_HASH_CLAUSE: &str = "/*ORDER_HASH_CLAUSE*/"; const ORDER_HASH_CLAUSE_BODY: &str = "AND tws.order_hash = {param}"; +const ORDER_HASHES_CLAUSE: &str = "/*ORDER_HASHES_CLAUSE*/"; +const ORDER_HASHES_CLAUSE_BODY: &str = "AND tws.order_hash IN ({list})"; const START_TS_CLAUSE: &str = "/*START_TS_CLAUSE*/"; const START_TS_BODY: &str = "AND tws.block_timestamp >= {param}"; const END_TS_CLAUSE: &str = "/*END_TS_CLAUSE*/"; @@ -48,6 +59,7 @@ pub struct FetchTradesArgs { pub owners: Vec
, pub takers: Vec
, pub order_hash: Option, + pub order_hashes: Vec, pub tokens: FetchTradesTokensFilter, pub time_filter: TimeFilter, pub pagination: PaginationParams, @@ -114,6 +126,35 @@ pub fn build_fetch_trades_stmt(args: &FetchTradesArgs) -> Result end { @@ -240,7 +281,10 @@ fn bind_token_filters( #[cfg(test)] mod tests { use super::*; - use alloy::{hex, primitives::address}; + use alloy::{ + hex, + primitives::{address, b256}, + }; #[test] fn builds_with_chain_ids() { @@ -333,6 +377,36 @@ mod tests { ); } + #[test] + fn builds_with_batch_order_hash_filters() { + let hash_a = b256!("0x1111111111111111111111111111111111111111111111111111111111111111"); + let hash_b = b256!("0x2222222222222222222222222222222222222222222222222222222222222222"); + let stmt = build_fetch_trades_stmt(&FetchTradesArgs { + order_hashes: vec![hash_b, hash_a, hash_a], + ..Default::default() + }) + .unwrap(); + + assert!(stmt + .sql + .contains("c.alice_order_hash IN (?1, ?2) OR c.bob_order_hash IN (?1, ?2)")); + assert!(stmt.sql.contains("oe.order_hash IN (?3, ?4)")); + assert!(stmt.sql.contains("mc.alice_order_hash IN (?5, ?6)")); + assert!(stmt.sql.contains("mc.bob_order_hash IN (?7, ?8)")); + assert!(stmt.sql.contains("tws.order_hash IN (?9, ?10)")); + assert!(!stmt.sql.contains(ORDER_HASHES_CLAUSE)); + assert!(!stmt.sql.contains(TAKE_TRADES_ORDER_HASHES_CLAUSE)); + assert!(!stmt.sql.contains(CLEAR_ALICE_ORDER_HASHES_CLAUSE)); + assert!(!stmt.sql.contains(CLEAR_BOB_ORDER_HASHES_CLAUSE)); + assert!(!stmt.sql.contains(CLEAR_EVENTS_ORDER_HASHES_CLAUSE)); + assert_eq!(stmt.params[0], SqlValue::Text(hex::encode_prefixed(hash_a))); + assert_eq!(stmt.params[1], SqlValue::Text(hex::encode_prefixed(hash_b))); + assert_eq!(stmt.params[6], SqlValue::Text(hex::encode_prefixed(hash_a))); + assert_eq!(stmt.params[7], SqlValue::Text(hex::encode_prefixed(hash_b))); + assert_eq!(stmt.params[8], SqlValue::Text(hex::encode_prefixed(hash_a))); + assert_eq!(stmt.params[9], SqlValue::Text(hex::encode_prefixed(hash_b))); + } + #[test] fn builds_with_same_token_as_either_side_filter() { let token = address!("0x1111111111111111111111111111111111111111"); diff --git a/crates/common/src/local_db/query/fetch_trades/query.sql b/crates/common/src/local_db/query/fetch_trades/query.sql index 6610fb7dbe..a5905c397a 100644 --- a/crates/common/src/local_db/query/fetch_trades/query.sql +++ b/crates/common/src/local_db/query/fetch_trades/query.sql @@ -45,6 +45,7 @@ matching_clears AS ( /*CLEAR_EVENTS_CHAIN_IDS_CLAUSE*/ /*CLEAR_EVENTS_ORDERBOOKS_CLAUSE*/ /*CLEAR_EVENTS_TAKERS_CLAUSE*/ + /*CLEAR_EVENTS_ORDER_HASHES_CLAUSE*/ ), take_trades AS ( SELECT @@ -77,6 +78,7 @@ take_trades AS ( oe.block_number < mt.block_number OR (oe.block_number = mt.block_number AND oe.log_index <= mt.log_index) ) + /*TAKE_TRADES_ORDER_HASHES_CLAUSE*/ AND NOT EXISTS ( SELECT 1 FROM order_events newer @@ -139,6 +141,7 @@ clear_alice AS ( oe.block_number < mc.block_number OR (oe.block_number = mc.block_number AND oe.log_index <= mc.log_index) ) + /*CLEAR_ALICE_ORDER_HASHES_CLAUSE*/ AND NOT EXISTS ( SELECT 1 FROM order_events newer @@ -212,6 +215,7 @@ clear_bob AS ( oe.block_number < mc.block_number OR (oe.block_number = mc.block_number AND oe.log_index <= mc.log_index) ) + /*CLEAR_BOB_ORDER_HASHES_CLAUSE*/ AND NOT EXISTS ( SELECT 1 FROM order_events newer @@ -374,6 +378,7 @@ LEFT JOIN erc20_tokens tok_out WHERE 1 = 1 /*OWNERS_CLAUSE*/ /*ORDER_HASH_CLAUSE*/ +/*ORDER_HASHES_CLAUSE*/ /*START_TS_CLAUSE*/ /*END_TS_CLAUSE*/ /*INPUT_TOKENS_CLAUSE*/ diff --git a/crates/common/src/raindex_client/trades/get_all.rs b/crates/common/src/raindex_client/trades/get_all.rs index 31ba1e1cdc..b7599414f2 100644 --- a/crates/common/src/raindex_client/trades/get_all.rs +++ b/crates/common/src/raindex_client/trades/get_all.rs @@ -96,6 +96,7 @@ impl From for SgTradesListQueryFilters { .map(|owner| SgBytes(owner.to_string())) .collect(), order_hash: filters.order_hash.map(|hash| SgBytes(hash.to_string())), + order_hash_in: Vec::new(), }); } @@ -131,7 +132,9 @@ fn local_trades_pagination( } } -fn normalize_trade_tokens(mut tokens: SgTradesTokensFilterArgs) -> SgTradesTokensFilterArgs { +pub(super) fn normalize_trade_tokens( + mut tokens: SgTradesTokensFilterArgs, +) -> SgTradesTokensFilterArgs { tokens.inputs.sort_unstable(); tokens.inputs.dedup(); tokens.outputs.sort_unstable(); @@ -139,7 +142,10 @@ fn normalize_trade_tokens(mut tokens: SgTradesTokensFilterArgs) -> SgTradesToken tokens } -fn sg_trade_matches_token_filter(trade: &SgTrade, tokens: &SgTradesTokensFilterArgs) -> bool { +pub(super) fn sg_trade_matches_token_filter( + trade: &SgTrade, + tokens: &SgTradesTokensFilterArgs, +) -> bool { let has_inputs = !tokens.inputs.is_empty(); let has_outputs = !tokens.outputs.is_empty(); let input_token = trade @@ -246,6 +252,7 @@ impl RaindexClient { owners: filters.owners.clone(), takers: filters.takers.clone(), order_hash: filters.order_hash, + order_hashes: Vec::new(), tokens: local_tokens.clone(), time_filter: filters.time_filter.clone().unwrap_or_default(), pagination: local_pagination, @@ -276,6 +283,7 @@ impl RaindexClient { owners: filters.owners.clone(), takers: filters.takers.clone(), order_hash: filters.order_hash, + order_hashes: Vec::new(), tokens: local_tokens, time_filter: filters.time_filter.clone().unwrap_or_default(), pagination: PaginationParams::default(), diff --git a/crates/common/src/raindex_client/trades/get_by_order_hashes.rs b/crates/common/src/raindex_client/trades/get_by_order_hashes.rs new file mode 100644 index 0000000000..781d8c98b3 --- /dev/null +++ b/crates/common/src/raindex_client/trades/get_by_order_hashes.rs @@ -0,0 +1,462 @@ +use super::get_all::{normalize_trade_tokens, sg_trade_matches_token_filter, GetTradesTokenFilter}; +use super::*; +use crate::local_db::query::fetch_trades::{FetchTradesArgs, FetchTradesTokensFilter}; +use crate::raindex_client::local_db::query::fetch_trades::fetch_trades; +use crate::utils::timing::Timing; +use rain_orderbook_subgraph_client::types::common::{ + SgBigInt, SgBytes, SgTradeEventFilter, SgTradeOrderFilter, SgTradesListQueryFilters, + SgTradesTokensFilterArgs, +}; +use rain_orderbook_subgraph_client::MultiOrderbookSubgraphClient; +use std::collections::{HashMap, HashSet}; +use tsify::Tsify; +use wasm_bindgen_utils::{impl_wasm_traits, wasm_export}; + +#[derive(Serialize, Deserialize, Debug, Clone, Tsify)] +pub struct OrderHashes(#[tsify(type = "Hex[]")] pub Vec); +impl_wasm_traits!(OrderHashes); + +#[derive(Serialize, Deserialize, Debug, Clone, Tsify, Default)] +#[serde(rename_all = "camelCase", default)] +pub struct GetTradesByOrderHashesFilters { + #[tsify(optional, type = "Address[]")] + pub owners: Vec
, + #[tsify(optional, type = "Address[]")] + pub takers: Vec
, + #[tsify(optional)] + pub tokens: Option, + #[tsify(optional, type = "Address[]")] + pub orderbook_addresses: Option>, + #[tsify(optional)] + pub time_filter: Option, +} +impl_wasm_traits!(GetTradesByOrderHashesFilters); + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[wasm_bindgen] +pub struct RaindexOrderHashTrades { + order_hash: B256, + trades: Vec, +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen] +impl RaindexOrderHashTrades { + #[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 = "RaindexTrade[]")] + pub fn trades(&self) -> Vec { + self.trades.clone() + } +} + +#[cfg(not(target_family = "wasm"))] +impl RaindexOrderHashTrades { + pub fn order_hash(&self) -> B256 { + self.order_hash + } + + pub fn trades(&self) -> &[RaindexTrade] { + &self.trades + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[wasm_bindgen] +pub struct RaindexTradesByOrderHashResult { + trades_by_order_hash: Vec, + total_count: u64, +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen] +impl RaindexTradesByOrderHashResult { + #[wasm_bindgen(getter = tradesByOrderHash, unchecked_return_type = "RaindexOrderHashTrades[]")] + pub fn trades_by_order_hash(&self) -> Vec { + self.trades_by_order_hash.clone() + } + + #[wasm_bindgen(getter = totalCount)] + pub fn total_count(&self) -> u64 { + self.total_count + } +} + +#[cfg(not(target_family = "wasm"))] +impl RaindexTradesByOrderHashResult { + pub fn trades_by_order_hash(&self) -> &[RaindexOrderHashTrades] { + &self.trades_by_order_hash + } + + pub fn total_count(&self) -> u64 { + self.total_count + } +} + +fn unique_order_hashes_preserving_order(order_hashes: Vec) -> Vec { + let mut seen = HashSet::new(); + let mut unique = Vec::with_capacity(order_hashes.len()); + for order_hash in order_hashes { + if seen.insert(order_hash) { + unique.push(order_hash); + } + } + unique +} + +fn build_sg_filters( + filters: GetTradesByOrderHashesFilters, + order_hashes: &[B256], +) -> SgTradesListQueryFilters { + let time_filter = filters.time_filter.unwrap_or_default(); + let mut sg_filters = SgTradesListQueryFilters { + timestamp_gte: Some( + time_filter + .start + .map_or(SgBigInt("0".to_string()), |v| SgBigInt(v.to_string())), + ), + timestamp_lte: Some( + time_filter + .end + .map_or(SgBigInt(u64::MAX.to_string()), |v| SgBigInt(v.to_string())), + ), + orderbook_in: filters + .orderbook_addresses + .unwrap_or_default() + .into_iter() + .map(|address| address.to_string().to_lowercase()) + .collect(), + ..Default::default() + }; + + sg_filters.order_ = Some(SgTradeOrderFilter { + owner_in: filters + .owners + .into_iter() + .map(|owner| SgBytes(owner.to_string())) + .collect(), + order_hash: None, + order_hash_in: order_hashes + .iter() + .map(|hash| SgBytes(hash.to_string())) + .collect(), + }); + + if !filters.takers.is_empty() { + sg_filters.trade_event_ = Some(SgTradeEventFilter { + sender_in: filters + .takers + .into_iter() + .map(|taker| SgBytes(taker.to_string())) + .collect(), + }); + } + + sg_filters +} + +fn group_trades_by_order_hash( + order_hashes: Vec, + mut trades: Vec, +) -> RaindexTradesByOrderHashResult { + trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp).then_with(|| a.id.cmp(&b.id))); + let total_count = trades.len() as u64; + let mut grouped: HashMap> = HashMap::new(); + for trade in trades { + grouped.entry(trade.order_hash).or_default().push(trade); + } + + let trades_by_order_hash = order_hashes + .into_iter() + .map(|order_hash| RaindexOrderHashTrades { + order_hash, + trades: grouped.remove(&order_hash).unwrap_or_default(), + }) + .collect(); + + RaindexTradesByOrderHashResult { + trades_by_order_hash, + total_count, + } +} + +#[wasm_export] +impl RaindexClient { + #[wasm_export( + js_name = "getTradesByOrderHashes", + return_description = "Trades grouped by requested order hash", + unchecked_return_type = "RaindexTradesByOrderHashResult", + preserve_js_class + )] + pub async fn get_trades_by_order_hashes( + &self, + #[wasm_export( + js_name = "chainIds", + param_description = "Specific blockchain networks to query (optional, queries all networks if not specified)" + )] + chain_ids: Option, + #[wasm_export( + js_name = "orderHashes", + param_description = "Order hashes to query in a single batch", + unchecked_param_type = "Hex[]" + )] + order_hashes: OrderHashes, + #[wasm_export( + param_description = "Filtering criteria including owners, takers, token addresses, orderbooks, and time range" + )] + filters: Option, + ) -> Result { + let started = Timing::now(); + let requested_order_hashes_count = order_hashes.0.len(); + let order_hashes = unique_order_hashes_preserving_order(order_hashes.0); + if order_hashes.is_empty() { + return Ok(RaindexTradesByOrderHashResult { + trades_by_order_hash: Vec::new(), + total_count: 0, + }); + } + + let filters = filters.unwrap_or_default(); + let input_tokens_count = filters + .tokens + .as_ref() + .and_then(|tokens| tokens.inputs.as_ref()) + .map_or(0, Vec::len); + let output_tokens_count = filters + .tokens + .as_ref() + .and_then(|tokens| tokens.outputs.as_ref()) + .map_or(0, Vec::len); + let takers_count = filters.takers.len(); + let owners_count = filters.owners.len(); + let orderbooks_count = filters.orderbook_addresses.as_ref().map_or(0, Vec::len); + let ids = chain_ids.map(|ChainIds(ids)| ids); + let requested_chain_ids_count = ids.as_ref().map_or(0, Vec::len); + let (local_db, local_ids, sg_ids) = self.classify_chains(ids)?; + let local_chain_ids_count = local_ids.len(); + let subgraph_chain_ids_count = sg_ids.len(); + + let mut all_trades = Vec::new(); + let mut local_rows = 0usize; + let mut subgraph_rows_before_token_filter = 0usize; + let mut subgraph_rows_after_token_filter = 0usize; + + if let Some(db) = local_db { + let local_tokens = filters + .tokens + .clone() + .map(|tokens| FetchTradesTokensFilter { + inputs: tokens.inputs.unwrap_or_default(), + outputs: tokens.outputs.unwrap_or_default(), + }) + .unwrap_or_default(); + let local_started = Timing::now(); + let local_trades = fetch_trades( + &db, + FetchTradesArgs { + chain_ids: local_ids, + orderbook_addresses: filters.orderbook_addresses.clone().unwrap_or_default(), + owners: filters.owners.clone(), + takers: filters.takers.clone(), + order_hash: None, + order_hashes: order_hashes.clone(), + tokens: local_tokens, + time_filter: filters.time_filter.clone().unwrap_or_default(), + pagination: PaginationParams::default(), + }, + ) + .await?; + local_rows = local_trades.len(); + all_trades.extend( + local_trades + .into_iter() + .map(RaindexTrade::try_from_local_db_trade) + .collect::, _>>()?, + ); + tracing::debug!( + order_hashes_count = order_hashes.len(), + rows = local_rows, + duration_ms = local_started.elapsed_ms(), + "getTradesByOrderHashes local DB completed" + ); + } + + if !sg_ids.is_empty() { + let multi_subgraph_args = self.get_multi_subgraph_args(Some(sg_ids))?; + let sg_filters = build_sg_filters(filters.clone(), &order_hashes); + let sg_token_filter = filters + .tokens + .clone() + .and_then(Option::::from) + .map(normalize_trade_tokens); + let name_to_chain_id: 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 subgraph_started = Timing::now(); + let sg_trades = client.trades_list_all(sg_filters).await?; + subgraph_rows_before_token_filter = sg_trades.len(); + let sg_trades: Vec<_> = if let Some(tokens) = sg_token_filter { + sg_trades + .into_iter() + .filter(|trade| sg_trade_matches_token_filter(&trade.trade, &tokens)) + .collect() + } else { + sg_trades + }; + subgraph_rows_after_token_filter = sg_trades.len(); + 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(), + ))?; + all_trades.push(RaindexTrade::try_from_sg_trade( + chain_id, + trade_with_name.trade, + )?); + } + tracing::debug!( + order_hashes_count = order_hashes.len(), + rows_before_token_filter = subgraph_rows_before_token_filter, + rows_after_token_filter = subgraph_rows_after_token_filter, + duration_ms = subgraph_started.elapsed_ms(), + "getTradesByOrderHashes subgraph completed" + ); + } + + let result = group_trades_by_order_hash(order_hashes, all_trades); + tracing::debug!( + requested_chain_ids_count, + local_chain_ids_count, + subgraph_chain_ids_count, + requested_order_hashes_count, + unique_order_hashes_count = result.trades_by_order_hash.len(), + owners_count, + takers_count, + orderbooks_count, + input_tokens_count, + output_tokens_count, + local_rows, + subgraph_rows_before_token_filter, + subgraph_rows_after_token_filter, + total_count = result.total_count, + duration_ms = started.elapsed_ms(), + "getTradesByOrderHashes completed" + ); + + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::local_db::query::fetch_order_trades::LocalDbOrderTrade; + use alloy::primitives::{b256, Bytes}; + + fn local_trade(order_hash: B256, block_timestamp: u64, trade_id: u8) -> LocalDbOrderTrade { + LocalDbOrderTrade { + chain_id: 1, + trade_kind: "take".to_string(), + orderbook: Address::ZERO, + order_hash, + order_owner: Address::ZERO, + order_nonce: "0".to_string(), + transaction_hash: B256::ZERO, + log_index: 1, + block_number: 1, + block_timestamp, + transaction_sender: Address::ZERO, + input_vault_id: U256::from(1), + input_token: Address::ZERO, + input_token_name: None, + input_token_symbol: None, + input_token_decimals: Some(18), + input_delta: "0x0000000000000000000000000000000000000000000000000000000000000001" + .to_string(), + input_running_balance: Some( + "0x0000000000000000000000000000000000000000000000000000000000000001".to_string(), + ), + output_vault_id: U256::from(2), + output_token: Address::ZERO, + output_token_name: None, + output_token_symbol: None, + output_token_decimals: Some(18), + output_delta: "0x00000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffe" + .to_string(), + output_running_balance: Some( + "0x0000000000000000000000000000000000000000000000000000000000000001".to_string(), + ), + trade_id: Bytes::from(vec![trade_id]).to_string(), + } + } + + #[test] + fn unique_order_hashes_preserves_first_seen_order() { + let hash_a = b256!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + let hash_b = b256!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + assert_eq!( + unique_order_hashes_preserving_order(vec![hash_b, hash_a, hash_b]), + vec![hash_b, hash_a] + ); + } + + #[test] + fn groups_requested_hashes_and_preserves_empty_buckets() { + let hash_a = b256!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + let hash_b = b256!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + let trade = RaindexTrade::try_from_local_db_trade(local_trade(hash_b, 1, 1)).unwrap(); + + let result = group_trades_by_order_hash(vec![hash_a, hash_b], vec![trade]); + + assert_eq!(result.total_count(), 1); + assert_eq!(result.trades_by_order_hash[0].order_hash, hash_a); + assert!(result.trades_by_order_hash[0].trades.is_empty()); + assert_eq!(result.trades_by_order_hash[1].order_hash, hash_b); + assert_eq!(result.trades_by_order_hash[1].trades.len(), 1); + } + + #[test] + fn sorts_grouped_trades_by_timestamp_desc() { + let hash = b256!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + let result = group_trades_by_order_hash( + vec![hash], + vec![ + RaindexTrade::try_from_local_db_trade(local_trade(hash, 1, 1)).unwrap(), + RaindexTrade::try_from_local_db_trade(local_trade(hash, 2, 2)).unwrap(), + ], + ); + + assert_eq!( + result.trades_by_order_hash[0].trades[0].timestamp, + U256::from(2) + ); + } + + #[test] + fn builds_subgraph_order_hash_in_filter() { + let hash_a = b256!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + let hash_b = b256!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + let filters = build_sg_filters(GetTradesByOrderHashesFilters::default(), &[hash_a, hash_b]); + let order_filter = filters.order_.unwrap(); + + assert_eq!(order_filter.order_hash, None); + assert_eq!( + order_filter.order_hash_in, + vec![SgBytes(hash_a.to_string()), SgBytes(hash_b.to_string())] + ); + } +} diff --git a/crates/common/src/raindex_client/trades/mod.rs b/crates/common/src/raindex_client/trades/mod.rs index f1f060affa..edce36e30c 100644 --- a/crates/common/src/raindex_client/trades/mod.rs +++ b/crates/common/src/raindex_client/trades/mod.rs @@ -1,8 +1,13 @@ mod get_all; +mod get_by_order_hashes; mod get_by_owner; mod get_by_tx; pub use get_all::{GetTradesFilters, GetTradesTokenFilter}; +pub use get_by_order_hashes::{ + GetTradesByOrderHashesFilters, OrderHashes, RaindexOrderHashTrades, + RaindexTradesByOrderHashResult, +}; use super::local_db::orders::LocalDbOrders; use super::orders::{OrdersDataSource, SubgraphOrders}; diff --git a/crates/subgraph/src/types/common.rs b/crates/subgraph/src/types/common.rs index 6744578a49..0cd17de966 100644 --- a/crates/subgraph/src/types/common.rs +++ b/crates/subgraph/src/types/common.rs @@ -155,6 +155,8 @@ pub struct SgTradeOrderFilter { #[cynic(rename = "orderHash", skip_serializing_if = "Option::is_none")] #[cfg_attr(target_family = "wasm", tsify(optional))] pub order_hash: Option, + #[cynic(rename = "orderHash_in", skip_serializing_if = "Vec::is_empty")] + pub order_hash_in: Vec, } #[derive(cynic::InputObject, Debug, Clone, Tsify, Default)]