From 957f5f0a0cdad56dfdb52ed68067fc0fe4a74464 Mon Sep 17 00:00:00 2001
From: findolor <16416963+findolor@users.noreply.github.com>
Date: Wed, 13 May 2026 15:41:34 +0000
Subject: [PATCH] feat: add batch order hash trade lookup (#2572)
## Related Issue
- [RAI-527: Add order hash batch query support to raindex trades SDK](https://linear.app/makeitrain/issue/RAI-527/add-order-hash-batch-query-support-to-raindex-trades-sdk)
## Dependent PRs
- Depends on #2571
## Motivation
Consumers need to fetch trades for multiple order hashes without issuing one SDK query per hash. Running many independent trade lookups would duplicate local DB reconstruction work and subgraph pagination work, especially when callers need results for dozens of orders.
## Solution
- Add `RaindexClient.getTradesByOrderHashes`, exposed to wasm as `getTradesByOrderHashes`, returning trades grouped by requested order hash.
- Preserve requested order-hash ordering, dedupe duplicate requested hashes, and return empty buckets for hashes with no matching trades.
- Add optional owner, taker, token, orderbook, and time filters to the batch API.
- Extend local DB `FetchTradesArgs` with `order_hashes` and push the batch predicates into the trade source CTEs, including `matching_clears`, `take_trades`, `clear_alice`, and `clear_bob`.
- Add subgraph `orderHash_in` support and issue one `trades_list_all` request for all requested hashes.
- Reuse the existing token-filter normalization/matching logic for subgraph results.
- Add debug tracing for local DB, subgraph, and total batch lookup timing and row counts.
- Add focused tests for SQL generation, subgraph filter construction, hash deduping, grouping, empty buckets, and result ordering.
## Checks
By submitting this for review, I confirm I have done the following:
- [x] made this PR as small as possible
- [x] unit-tested any new functionality
- [x] linked any relevant issues or PRs
- [ ] included screenshots (if this involves a front-end change)
Additional validation run:
- `cargo fmt --all`
- `nix develop -c cargo test -p rain_orderbook_common fetch_trades`
- `nix develop -c cargo test -p rain_orderbook_common get_by_order_hashes`
- `nix develop -c cargo test -p rain_orderbook_common`
- `nix develop -c cargo test -p rain_orderbook_subgraph_client`
- `nix develop -c rainix-wasm-test`
- `nix develop -c rainix-rs-static`
## Summary by CodeRabbit
* **New Features**
* Added batch filtering by multiple order hashes in trade queries.
* Introduced API to fetch trades grouped by order hashes, with optional filtering by owners, takers, tokens, orderbook addresses, and time range.
[](https://app.coderabbit.ai/change-stack/rainlanguage/raindex/pull/2572)
---
.../src/local_db/query/fetch_trades/mod.rs | 76 ++-
.../src/local_db/query/fetch_trades/query.sql | 5 +
.../src/raindex_client/trades/get_all.rs | 12 +-
.../trades/get_by_order_hashes.rs | 462 ++++++++++++++++++
.../common/src/raindex_client/trades/mod.rs | 5 +
crates/subgraph/src/types/common.rs | 2 +
6 files changed, 559 insertions(+), 3 deletions(-)
create mode 100644 crates/common/src/raindex_client/trades/get_by_order_hashes.rs
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)]