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)]