From 0ff41082c1b3bfe30f5551c3757b5eec2e05c6be Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 5 Dec 2025 13:35:43 +0300 Subject: [PATCH 1/7] rename error --- crates/common/src/raindex_client/add_orders.rs | 4 ++-- crates/common/src/raindex_client/mod.rs | 8 ++++---- crates/common/src/raindex_client/remove_orders.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/common/src/raindex_client/add_orders.rs b/crates/common/src/raindex_client/add_orders.rs index 835a75aaa5..7fc5d86790 100644 --- a/crates/common/src/raindex_client/add_orders.rs +++ b/crates/common/src/raindex_client/add_orders.rs @@ -163,7 +163,7 @@ impl RaindexClient { } } - Err(RaindexError::SubgraphIndexingTimeout { tx_hash, attempts }) + Err(RaindexError::TransactionIndexingTimeout { tx_hash, attempts }) } } @@ -576,7 +576,7 @@ mod tests { .unwrap_err(); match err { - RaindexError::SubgraphIndexingTimeout { attempts, .. } => { + RaindexError::TransactionIndexingTimeout { attempts, .. } => { assert_eq!(attempts, DEFAULT_ADD_ORDER_POLL_ATTEMPTS); } other => panic!("expected timeout error, got {other:?}"), diff --git a/crates/common/src/raindex_client/mod.rs b/crates/common/src/raindex_client/mod.rs index 6f0ce13c42..952eb6c2dc 100644 --- a/crates/common/src/raindex_client/mod.rs +++ b/crates/common/src/raindex_client/mod.rs @@ -261,8 +261,8 @@ pub enum RaindexError { NoNetworksConfigured, #[error("Subgraph not configured for chain ID: {0}")] SubgraphNotConfigured(String), - #[error("Subgraph did not index transaction {tx_hash:#x} after {attempts} attempts")] - SubgraphIndexingTimeout { tx_hash: B256, attempts: usize }, + #[error("Transaction {tx_hash:#x} was not indexed after {attempts} attempts")] + TransactionIndexingTimeout { tx_hash: B256, attempts: usize }, #[error(transparent)] YamlError(#[from] YamlError), #[error(transparent)] @@ -365,9 +365,9 @@ impl RaindexError { RaindexError::SubgraphNotConfigured(chain_id) => { format!("No subgraph is configured for chain ID '{}'.", chain_id) } - RaindexError::SubgraphIndexingTimeout { tx_hash, attempts } => { + RaindexError::TransactionIndexingTimeout { tx_hash, attempts } => { format!( - "Timeout waiting for the subgraph to index transaction {tx_hash:#x} after {attempts} attempts." + "Timeout waiting for transaction {tx_hash:#x} to be indexed after {attempts} attempts." ) } RaindexError::YamlError(err) => format!( diff --git a/crates/common/src/raindex_client/remove_orders.rs b/crates/common/src/raindex_client/remove_orders.rs index 2a8ce61472..99341fc692 100644 --- a/crates/common/src/raindex_client/remove_orders.rs +++ b/crates/common/src/raindex_client/remove_orders.rs @@ -167,7 +167,7 @@ impl RaindexClient { } } - Err(RaindexError::SubgraphIndexingTimeout { tx_hash, attempts }) + Err(RaindexError::TransactionIndexingTimeout { tx_hash, attempts }) } } @@ -682,7 +682,7 @@ mod tests { .unwrap_err(); match err { - RaindexError::SubgraphIndexingTimeout { attempts, .. } => { + RaindexError::TransactionIndexingTimeout { attempts, .. } => { assert_eq!(attempts, DEFAULT_REMOVE_ORDER_POLL_ATTEMPTS); } other => panic!("expected timeout error, got {other:?}"), From 1f121a3e752450f3c102c706f7b37fbdc9cc808e Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 5 Dec 2025 13:37:43 +0300 Subject: [PATCH 2/7] add database query to fetch a transaction by hash --- .../query/fetch_transaction_by_hash/mod.rs | 54 ++++++++++++ .../query/fetch_transaction_by_hash/query.sql | 19 ++++ crates/common/src/local_db/query/mod.rs | 1 + .../query/fetch_transaction_by_hash.rs | 87 +++++++++++++++++++ .../src/raindex_client/local_db/query/mod.rs | 1 + 5 files changed, 162 insertions(+) create mode 100644 crates/common/src/local_db/query/fetch_transaction_by_hash/mod.rs create mode 100644 crates/common/src/local_db/query/fetch_transaction_by_hash/query.sql create mode 100644 crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs diff --git a/crates/common/src/local_db/query/fetch_transaction_by_hash/mod.rs b/crates/common/src/local_db/query/fetch_transaction_by_hash/mod.rs new file mode 100644 index 0000000000..69c205b3fd --- /dev/null +++ b/crates/common/src/local_db/query/fetch_transaction_by_hash/mod.rs @@ -0,0 +1,54 @@ +use crate::local_db::{ + query::{SqlStatement, SqlValue}, + OrderbookIdentifier, +}; +use alloy::primitives::{Address, B256}; +use serde::{Deserialize, Serialize}; + +const QUERY_TEMPLATE: &str = include_str!("query.sql"); + +/// Transaction info returned from local DB query +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LocalDbTransaction { + pub transaction_hash: B256, + pub block_number: u64, + pub block_timestamp: u64, + pub owner: Address, +} + +/// Builds a SQL statement to fetch transaction info by transaction hash +/// from the vault_balance_changes table. +pub fn build_fetch_transaction_by_hash_stmt( + ob_id: &OrderbookIdentifier, + tx_hash: B256, +) -> SqlStatement { + SqlStatement::new_with_params( + QUERY_TEMPLATE, + vec![ + SqlValue::from(ob_id.chain_id), + SqlValue::from(ob_id.orderbook_address), + SqlValue::from(tx_hash), + ], + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::{address, b256}; + + #[test] + fn builds_correct_sql_with_params() { + let orderbook = address!("0x1234567890123456789012345678901234567890"); + let tx_hash = b256!("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"); + let ob_id = OrderbookIdentifier::new(1, orderbook); + + let stmt = build_fetch_transaction_by_hash_stmt(&ob_id, tx_hash); + + assert!(stmt.sql.contains("SELECT")); + assert!(stmt.sql.contains("FROM vault_balance_changes")); + assert!(stmt.sql.contains("transaction_hash")); + assert_eq!(stmt.params.len(), 3); + } +} diff --git a/crates/common/src/local_db/query/fetch_transaction_by_hash/query.sql b/crates/common/src/local_db/query/fetch_transaction_by_hash/query.sql new file mode 100644 index 0000000000..6104676a3e --- /dev/null +++ b/crates/common/src/local_db/query/fetch_transaction_by_hash/query.sql @@ -0,0 +1,19 @@ +WITH params AS ( + SELECT + ?1 AS chain_id, + ?2 AS orderbook_address, + ?3 AS transaction_hash +) +SELECT + vbc.transaction_hash AS transactionHash, + vbc.block_number AS blockNumber, + vbc.block_timestamp AS blockTimestamp, + vbc.owner +FROM vault_balance_changes vbc +JOIN params p + ON p.chain_id = vbc.chain_id + AND p.orderbook_address = vbc.orderbook_address + AND p.transaction_hash = vbc.transaction_hash +ORDER BY vbc.log_index ASC +LIMIT 1; + diff --git a/crates/common/src/local_db/query/mod.rs b/crates/common/src/local_db/query/mod.rs index 924cc9bd9e..db272500d6 100644 --- a/crates/common/src/local_db/query/mod.rs +++ b/crates/common/src/local_db/query/mod.rs @@ -12,6 +12,7 @@ pub mod fetch_orders; pub mod fetch_store_addresses; pub mod fetch_tables; pub mod fetch_target_watermark; +pub mod fetch_transaction_by_hash; pub mod fetch_vault_balance_changes; pub mod fetch_vaults; pub mod insert_db_metadata; diff --git a/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs b/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs new file mode 100644 index 0000000000..66a86d2a15 --- /dev/null +++ b/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs @@ -0,0 +1,87 @@ +use crate::local_db::query::fetch_transaction_by_hash::{ + build_fetch_transaction_by_hash_stmt, LocalDbTransaction, +}; +use crate::local_db::query::{LocalDbQueryError, LocalDbQueryExecutor}; +use crate::local_db::OrderbookIdentifier; +use alloy::primitives::B256; + +pub async fn fetch_transaction_by_hash( + exec: &E, + ob_id: &OrderbookIdentifier, + tx_hash: B256, +) -> Result, LocalDbQueryError> { + let stmt = build_fetch_transaction_by_hash_stmt(ob_id, tx_hash); + 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::{address, b256, Address}; + use std::cell::RefCell; + use std::rc::Rc; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + async fn wrapper_uses_builder_sql_exactly() { + let tx_hash = b256!("0x0000000000000000000000000000000000000000000000000000000000000abc"); + let orderbook = Address::from([0x51; 20]); + let expected_stmt = + build_fetch_transaction_by_hash_stmt(&OrderbookIdentifier::new(1, orderbook), tx_hash); + + 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 = super::fetch_transaction_by_hash( + &exec, + &OrderbookIdentifier::new(1, orderbook), + tx_hash, + ) + .await; + assert!(res.is_ok()); + assert_eq!(store.borrow().clone().0, expected_stmt.sql); + } + + #[wasm_bindgen_test] + async fn wrapper_returns_rows_when_present() { + let tx_hash = b256!("0x0000000000000000000000000000000000000000000000000000000000000abc"); + let orderbook = address!("0x5151515151515151515151515151515151515151"); + let owner = address!("0x1111111111111111111111111111111111111111"); + let expected_stmt = + build_fetch_transaction_by_hash_stmt(&OrderbookIdentifier::new(1, orderbook), tx_hash); + + let row_json = format!( + r#"[{{ + "transactionHash":"{}", + "blockNumber":100, + "blockTimestamp":999, + "owner":"{}" + }}]"#, + tx_hash, owner + ); + + let store = Rc::new(RefCell::new(( + String::new(), + wasm_bindgen::JsValue::UNDEFINED, + ))); + let callback = create_sql_capturing_callback(&row_json, store.clone()); + let exec = JsCallbackExecutor::from_ref(&callback); + + let res = super::fetch_transaction_by_hash( + &exec, + &OrderbookIdentifier::new(1, orderbook), + tx_hash, + ) + .await; + assert!(res.is_ok()); + let rows = res.unwrap(); + assert_eq!(rows.len(), 1); + assert_eq!(store.borrow().clone().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 d66c45d8ce..bface716de 100644 --- a/crates/common/src/raindex_client/local_db/query/mod.rs +++ b/crates/common/src/raindex_client/local_db/query/mod.rs @@ -7,6 +7,7 @@ pub mod fetch_order_trades_count; pub mod fetch_orders; pub mod fetch_store_addresses; pub mod fetch_tables; +pub mod fetch_transaction_by_hash; pub mod fetch_vault_balance_changes; pub mod fetch_vaults; pub mod update_last_synced_block; From 09322344f42ab033be37e2989264bb8252a717a7 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 5 Dec 2025 13:41:23 +0300 Subject: [PATCH 3/7] add transaction related polling mechanism --- .../common/src/raindex_client/local_db/mod.rs | 1 + .../raindex_client/local_db/transactions.rs | 131 +++++++++++ .../common/src/raindex_client/transactions.rs | 214 ++++++++++++++++-- 3 files changed, 329 insertions(+), 17 deletions(-) create mode 100644 crates/common/src/raindex_client/local_db/transactions.rs diff --git a/crates/common/src/raindex_client/local_db/mod.rs b/crates/common/src/raindex_client/local_db/mod.rs index e613bbedae..55779999f4 100644 --- a/crates/common/src/raindex_client/local_db/mod.rs +++ b/crates/common/src/raindex_client/local_db/mod.rs @@ -15,6 +15,7 @@ pub mod executor; pub mod orders; pub mod pipeline; pub mod query; +pub mod transactions; pub mod vaults; type ExecuteBatchFn = diff --git a/crates/common/src/raindex_client/local_db/transactions.rs b/crates/common/src/raindex_client/local_db/transactions.rs new file mode 100644 index 0000000000..312775814d --- /dev/null +++ b/crates/common/src/raindex_client/local_db/transactions.rs @@ -0,0 +1,131 @@ +use super::super::transactions::RaindexTransaction; +use super::super::RaindexError; +use super::LocalDb; +use crate::local_db::query::fetch_transaction_by_hash::{ + build_fetch_transaction_by_hash_stmt, LocalDbTransaction, +}; +use crate::local_db::query::LocalDbQueryExecutor; +use crate::local_db::OrderbookIdentifier; +use alloy::primitives::B256; + +pub struct LocalDbTransactions<'a> { + pub(crate) db: &'a LocalDb, +} + +impl<'a> LocalDbTransactions<'a> { + pub(crate) fn new(db: &'a LocalDb) -> Self { + Self { db } + } + + /// Fetch transaction info by transaction hash from the local DB. + /// Returns None if no transaction with that hash is found. + pub async fn get_by_tx_hash( + &self, + ob_id: &OrderbookIdentifier, + tx_hash: B256, + ) -> Result, RaindexError> { + let stmt = build_fetch_transaction_by_hash_stmt(ob_id, tx_hash); + let results: Vec = self.db.query_json(&stmt).await?; + + if let Some(local_tx) = results.into_iter().next() { + let tx = RaindexTransaction::from_local_parts( + local_tx.transaction_hash, + local_tx.owner, + local_tx.block_number, + local_tx.block_timestamp, + )?; + return Ok(Some(tx)); + } + + Ok(None) + } +} + +#[cfg(test)] +mod tests { + #[cfg(target_family = "wasm")] + use super::*; + + #[cfg(target_family = "wasm")] + mod wasm_tests { + use super::*; + use crate::raindex_client::local_db::executor::JsCallbackExecutor; + use crate::raindex_client::local_db::LocalDb; + use alloy::primitives::{address, b256}; + use serde_json::json; + use std::cell::RefCell; + use std::rc::Rc; + use wasm_bindgen::prelude::*; + use wasm_bindgen_test::wasm_bindgen_test; + use wasm_bindgen_utils::prelude::*; + + fn create_mock_callback(response_json: &str) -> js_sys::Function { + let json_str = response_json.to_string(); + let result = WasmEncodedResult::Success:: { + value: json_str, + error: None, + }; + let payload = js_sys::JSON::stringify(&serde_wasm_bindgen::to_value(&result).unwrap()) + .unwrap() + .as_string() + .unwrap(); + + let closure = + Closure::wrap(Box::new(move |_sql: String, _params: JsValue| -> JsValue { + js_sys::JSON::parse(&payload).unwrap() + }) + as Box JsValue>); + + closure.into_js_value().dyn_into().unwrap() + } + + #[wasm_bindgen_test] + async fn test_get_by_tx_hash_returns_transaction_when_found() { + let tx_hash = + b256!("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"); + let owner = address!("0x1111111111111111111111111111111111111111"); + let orderbook = address!("0x2222222222222222222222222222222222222222"); + + let tx_json = json!([{ + "transactionHash": tx_hash.to_string(), + "blockNumber": 12345, + "blockTimestamp": 1700000000, + "owner": owner.to_string() + }]); + + let callback = create_mock_callback(&tx_json.to_string()); + let exec = JsCallbackExecutor::from_ref(&callback); + let local_db = LocalDb::new(exec); + + let transactions = LocalDbTransactions::new(&local_db); + let ob_id = OrderbookIdentifier::new(1, orderbook); + + let result = transactions.get_by_tx_hash(&ob_id, tx_hash).await; + + assert!(result.is_ok()); + let tx = result.unwrap(); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert_eq!(tx.id().to_lowercase(), tx_hash.to_string().to_lowercase()); + } + + #[wasm_bindgen_test] + async fn test_get_by_tx_hash_returns_none_when_not_found() { + let tx_hash = + b256!("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"); + let orderbook = address!("0x2222222222222222222222222222222222222222"); + + let callback = create_mock_callback("[]"); + let exec = JsCallbackExecutor::from_ref(&callback); + let local_db = LocalDb::new(exec); + + let transactions = LocalDbTransactions::new(&local_db); + let ob_id = OrderbookIdentifier::new(1, orderbook); + + let result = transactions.get_by_tx_hash(&ob_id, tx_hash).await; + + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + } + } +} diff --git a/crates/common/src/raindex_client/transactions.rs b/crates/common/src/raindex_client/transactions.rs index 620ed73187..007a368d8e 100644 --- a/crates/common/src/raindex_client/transactions.rs +++ b/crates/common/src/raindex_client/transactions.rs @@ -1,12 +1,36 @@ use std::str::FromStr; use super::*; +use crate::local_db::is_chain_supported_local_db; +use crate::local_db::OrderbookIdentifier; +use crate::raindex_client::local_db::transactions::LocalDbTransactions; use alloy::primitives::{Address, B256, U256}; +#[cfg(target_family = "wasm")] +use gloo_timers::future::TimeoutFuture; use rain_orderbook_subgraph_client::types::{common::SgTransaction, Id}; +use rain_orderbook_subgraph_client::OrderbookSubgraphClientError; use serde::{Deserialize, Serialize}; +#[cfg(not(target_family = "wasm"))] +use std::time::Duration; +#[cfg(not(target_family = "wasm"))] +use tokio::time::sleep; #[cfg(target_family = "wasm")] use wasm_bindgen_utils::prelude::js_sys::BigInt; +const DEFAULT_TRANSACTION_POLL_ATTEMPTS: usize = 10; +const DEFAULT_TRANSACTION_POLL_INTERVAL_MS: u64 = 1_000; + +#[cfg(target_family = "wasm")] +async fn sleep_ms(ms: u64) { + let delay = ms.min(u32::MAX as u64) as u32; + TimeoutFuture::new(delay).await; +} + +#[cfg(not(target_family = "wasm"))] +async fn sleep_ms(ms: u64) { + sleep(Duration::from_millis(ms)).await; +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[wasm_bindgen] @@ -75,12 +99,15 @@ impl RaindexClient { /// Fetches transaction details for a given transaction hash /// /// Retrieves basic transaction information including sender, block number, - /// and timestamp. + /// and timestamp. Uses a two-phase polling mechanism: first polls the local DB + /// (if available and the chain is supported), then falls back to subgraph polling. /// /// ## Examples /// /// ```javascript /// const result = await client.getTransaction( + /// 1, + /// "0x1234567890123456789012345678901234567890", /// "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" /// ); /// if (result.error) { @@ -97,6 +124,8 @@ impl RaindexClient { )] pub async fn get_transaction_wasm_binding( &self, + #[wasm_export(js_name = "chainId", param_description = "Chain ID for the network")] + chain_id: u32, #[wasm_export( js_name = "orderbookAddress", param_description = "Orderbook contract address", @@ -109,23 +138,82 @@ impl RaindexClient { unchecked_param_type = "Hex" )] tx_hash: String, + #[wasm_export( + js_name = "maxAttempts", + param_description = "Optional maximum polling attempts before timing out" + )] + max_attempts: Option, + #[wasm_export( + js_name = "intervalMs", + param_description = "Optional polling interval in milliseconds" + )] + interval_ms: Option, ) -> Result { let orderbook_address = Address::from_str(&orderbook_address)?; let tx_hash = B256::from_str(&tx_hash)?; - self.get_transaction(orderbook_address, tx_hash).await + self.get_transaction( + chain_id, + orderbook_address, + tx_hash, + max_attempts.map(|v| v as usize), + interval_ms.map(|v| v as u64), + ) + .await } } impl RaindexClient { pub async fn get_transaction( &self, + chain_id: u32, orderbook_address: Address, tx_hash: B256, + max_attempts: Option, + interval_ms: Option, ) -> Result { let client = self.get_orderbook_client(orderbook_address)?; - let transaction = client - .transaction_detail(Id::new(tx_hash.to_string())) - .await?; - transaction.try_into() + + let attempts = max_attempts + .unwrap_or(DEFAULT_TRANSACTION_POLL_ATTEMPTS) + .max(1); + let interval_ms = interval_ms.unwrap_or(DEFAULT_TRANSACTION_POLL_INTERVAL_MS); + + // Phase 1: give the local DB the full polling window before touching subgraph + if let Some(local_db) = self.local_db() { + if is_chain_supported_local_db(chain_id) { + let local_source = LocalDbTransactions::new(&local_db); + let ob_id = OrderbookIdentifier::new(chain_id, orderbook_address); + + for attempt in 1..=attempts { + if let Some(tx) = local_source.get_by_tx_hash(&ob_id, tx_hash).await? { + return Ok(tx); + } + if attempt < attempts { + sleep_ms(interval_ms).await; + } + } + } + } + + // Phase 2: fall back to subgraph polling + for attempt in 1..=attempts { + match client + .transaction_detail(Id::new(tx_hash.to_string())) + .await + { + Ok(transaction) => { + return transaction.try_into(); + } + Err(OrderbookSubgraphClientError::Empty) => { + if attempt < attempts { + sleep_ms(interval_ms).await; + continue; + } + } + Err(e) => return Err(e.into()), + } + } + + Err(RaindexError::TransactionIndexingTimeout { tx_hash, attempts }) } } @@ -152,23 +240,36 @@ mod test_helpers { use crate::raindex_client::tests::{get_test_yaml, CHAIN_ID_1_ORDERBOOK_ADDRESS}; use alloy::primitives::b256; use httpmock::MockServer; - use serde_json::json; + use serde_json::{json, Value}; + + fn sample_transaction_response() -> Value { + json!({ + "data": { + "transaction": { + "id": "0x0000000000000000000000000000000000000000000000000000000000000123", + "from": "0x1000000000000000000000000000000000000000", + "blockNumber": "12345", + "timestamp": "1734054063" + } + } + }) + } + + fn empty_transaction_response() -> Value { + json!({ + "data": { + "transaction": null + } + }) + } #[tokio::test] async fn test_get_transaction() { let sg_server = MockServer::start_async().await; sg_server.mock(|when, then| { when.path("/sg"); - then.status(200).json_body_obj(&json!({ - "data": { - "transaction": { - "id": "0x0000000000000000000000000000000000000000000000000000000000000123", - "from": "0x1000000000000000000000000000000000000000", - "blockNumber": "12345", - "timestamp": "1734054063" - } - } - })); + then.status(200) + .json_body_obj(&sample_transaction_response()); }); let raindex_client = RaindexClient::new( @@ -183,8 +284,11 @@ mod test_helpers { .unwrap(); let tx = raindex_client .get_transaction( + 1, Address::from_str(CHAIN_ID_1_ORDERBOOK_ADDRESS).unwrap(), b256!("0x0000000000000000000000000000000000000000000000000000000000000123"), + None, + None, ) .await .unwrap(); @@ -199,5 +303,81 @@ mod test_helpers { assert_eq!(tx.block_number(), U256::from_str("12345").unwrap()); assert_eq!(tx.timestamp(), U256::from_str("1734054063").unwrap()); } + + #[tokio::test] + async fn test_get_transaction_with_polling_success() { + let sg_server = MockServer::start_async().await; + let _mock = sg_server.mock(|when, then| { + when.path("/sg"); + then.status(200) + .json_body_obj(&sample_transaction_response()); + }); + + let raindex_client = RaindexClient::new( + vec![get_test_yaml( + &sg_server.url("/sg"), + &sg_server.url("/sg"), + "http://localhost:3000", + "http://localhost:3000", + )], + None, + ) + .unwrap(); + + let tx = raindex_client + .get_transaction( + 1, + Address::from_str(CHAIN_ID_1_ORDERBOOK_ADDRESS).unwrap(), + b256!("0x0000000000000000000000000000000000000000000000000000000000000123"), + Some(DEFAULT_TRANSACTION_POLL_ATTEMPTS), + Some(10), + ) + .await + .unwrap(); + + assert_eq!( + tx.id(), + b256!("0x0000000000000000000000000000000000000000000000000000000000000123") + ); + } + + #[tokio::test] + async fn test_get_transaction_timeout() { + let sg_server = MockServer::start_async().await; + let _empty = sg_server.mock(|when, then| { + when.path("/sg"); + then.status(200) + .json_body_obj(&empty_transaction_response()); + }); + + let raindex_client = RaindexClient::new( + vec![get_test_yaml( + &sg_server.url("/sg"), + &sg_server.url("/sg"), + "http://localhost:3000", + "http://localhost:3000", + )], + None, + ) + .unwrap(); + + let err = raindex_client + .get_transaction( + 1, + Address::from_str(CHAIN_ID_1_ORDERBOOK_ADDRESS).unwrap(), + b256!("0x0000000000000000000000000000000000000000000000000000000000000123"), + Some(3), + Some(10), + ) + .await + .unwrap_err(); + + match err { + RaindexError::TransactionIndexingTimeout { attempts, .. } => { + assert_eq!(attempts, 3); + } + other => panic!("expected timeout error, got {other:?}"), + } + } } } From 8983bf8a6234c3ec0867df0b647947d4d2443fb6 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 5 Dec 2025 13:42:43 +0300 Subject: [PATCH 4/7] replace subgraph polling mechanism on webapp with sdk usage --- .../src/__tests__/TransactionManager.test.ts | 121 +++++++++++++++++- .../transactions/TransactionManager.ts | 80 ++---------- 2 files changed, 127 insertions(+), 74 deletions(-) diff --git a/packages/ui-components/src/__tests__/TransactionManager.test.ts b/packages/ui-components/src/__tests__/TransactionManager.test.ts index 20d2b3c034..813fa70aea 100644 --- a/packages/ui-components/src/__tests__/TransactionManager.test.ts +++ b/packages/ui-components/src/__tests__/TransactionManager.test.ts @@ -418,6 +418,46 @@ describe('TransactionManager', () => { links: expect.any(Array) }); }); + + it('should use SDK-based indexing via createSdkIndexingFn', async () => { + const mockTransaction = { execute: vi.fn() }; + vi.mocked(TransactionStore).mockImplementation( + () => mockTransaction as unknown as TransactionStore + ); + + await manager.createWithdrawTransaction(withdrawMockArgs); + + // Verify awaitIndexingFn was passed and is a function + const callArgs = vi.mocked(TransactionStore).mock.calls[0][0]; + expect(callArgs.awaitIndexingFn).toBeDefined(); + expect(typeof callArgs.awaitIndexingFn).toBe('function'); + + // Simulate calling the awaitIndexingFn to verify it calls the SDK + const mockContext: IndexingContext = { + updateState: vi.fn(), + onSuccess: vi.fn(), + onError: vi.fn(), + links: [] + }; + + // Mock a successful SDK response + vi.mocked(mockRaindexClient.getTransaction).mockResolvedValueOnce({ + value: { id: '0xwithdrawhash', from: '0xowner', blockNumber: 123n, timestamp: 1700000000n } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + await callArgs.awaitIndexingFn!(mockContext); + + // Verify the SDK method was called with correct arguments (chainId, orderbook, txHash) + expect(mockRaindexClient.getTransaction).toHaveBeenCalledWith( + withdrawMockArgs.chainId, + withdrawMockArgs.entity.orderbook, + withdrawMockArgs.txHash + ); + + // Verify success was called + expect(mockContext.onSuccess).toHaveBeenCalled(); + }); }); describe('transaction callbacks', () => { @@ -701,6 +741,82 @@ describe('TransactionManager', () => { links: expect.any(Array) }); }); + + it('should use SDK-based indexing via createSdkIndexingFn', async () => { + const mockTransaction = { execute: vi.fn() }; + vi.mocked(TransactionStore).mockImplementation( + () => mockTransaction as unknown as TransactionStore + ); + + await manager.createDepositTransaction(mockArgs); + + // Verify awaitIndexingFn was passed and is a function + const callArgs = vi.mocked(TransactionStore).mock.calls[0][0]; + expect(callArgs.awaitIndexingFn).toBeDefined(); + expect(typeof callArgs.awaitIndexingFn).toBe('function'); + + // Simulate calling the awaitIndexingFn to verify it calls the SDK + const mockContext: IndexingContext = { + updateState: vi.fn(), + onSuccess: vi.fn(), + onError: vi.fn(), + links: [] + }; + + // Mock a successful SDK response + vi.mocked(mockRaindexClient.getTransaction).mockResolvedValueOnce({ + value: { id: '0xdeposithash', from: '0xowner', blockNumber: 123n, timestamp: 1700000000n } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + await callArgs.awaitIndexingFn!(mockContext); + + // Verify the SDK method was called with correct arguments (chainId, orderbook, txHash) + expect(mockRaindexClient.getTransaction).toHaveBeenCalledWith( + mockArgs.chainId, + mockArgs.entity.orderbook, + mockArgs.txHash + ); + + // Verify success was called + expect(mockContext.onSuccess).toHaveBeenCalled(); + }); + + it('should handle SDK timeout error in awaitIndexingFn', async () => { + const mockTransaction = { execute: vi.fn() }; + vi.mocked(TransactionStore).mockImplementation( + () => mockTransaction as unknown as TransactionStore + ); + + await manager.createDepositTransaction(mockArgs); + + const callArgs = vi.mocked(TransactionStore).mock.calls[0][0]; + + const mockContext: IndexingContext = { + updateState: vi.fn(), + onSuccess: vi.fn(), + onError: vi.fn(), + links: [] + }; + + // Mock a timeout error from the SDK + vi.mocked(mockRaindexClient.getTransaction).mockResolvedValueOnce({ + error: { + readableMsg: 'Timeout waiting for transaction 0x123 to be indexed after 10 attempts.' + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + await callArgs.awaitIndexingFn!(mockContext); + + // Verify error handling + expect(mockContext.updateState).toHaveBeenCalledWith({ + status: TransactionStatusMessage.ERROR, + errorDetails: TransactionStoreErrorMessage.SUBGRAPH_TIMEOUT_ERROR + }); + expect(mockContext.onError).toHaveBeenCalled(); + expect(mockContext.onSuccess).not.toHaveBeenCalled(); + }); }); describe('createAddOrderTransaction', () => { @@ -951,11 +1067,10 @@ describe('createSdkIndexingFn', () => { }); }); - it('should set SUBGRAPH_TIMEOUT_ERROR for SDK SubgraphIndexingTimeout error format', async () => { + it('should set SUBGRAPH_TIMEOUT_ERROR for SDK TransactionIndexingTimeout error format', async () => { const mockCall = vi.fn().mockResolvedValue({ error: { - readableMsg: - 'Timeout waiting for the subgraph to index transaction 0x123abc after 10 attempts.' + readableMsg: 'Timeout waiting for transaction 0x123abc to be indexed after 10 attempts.' } } as WasmEncodedResult); const indexingFn = createSdkIndexingFn({ diff --git a/packages/ui-components/src/lib/providers/transactions/TransactionManager.ts b/packages/ui-components/src/lib/providers/transactions/TransactionManager.ts index dee2806acb..c2c119562a 100644 --- a/packages/ui-components/src/lib/providers/transactions/TransactionManager.ts +++ b/packages/ui-components/src/lib/providers/transactions/TransactionManager.ts @@ -14,16 +14,11 @@ import { import type { Config } from '@wagmi/core'; import type { ToastLink, ToastProps } from '$lib/types/toast'; import { getExplorerLink } from '$lib/services/getExplorerLink'; -import { - awaitSubgraphIndexing, - type AwaitSubgraphConfig -} from '$lib/services/awaitTransactionIndexing'; import { type RaindexVault, type RaindexOrder, RaindexClient, type Address, - type Hex, Float, type WasmEncodedResult } from '@rainlanguage/orderbook'; @@ -38,48 +33,6 @@ import { */ export type AddToastFunction = (toast: Omit) => void; -/** - * Creates an indexing function that wraps the legacy subgraph polling logic. - * This allows existing flows to work with the new generic indexing interface. - */ -function createSubgraphIndexingFn(config: AwaitSubgraphConfig) { - return async (ctx: IndexingContext): Promise => { - ctx.updateState({ status: TransactionStatusMessage.PENDING_SUBGRAPH }); - - const result = await awaitSubgraphIndexing(config); - - if (result.error === TransactionStoreErrorMessage.SUBGRAPH_TIMEOUT_ERROR) { - ctx.updateState({ - status: TransactionStatusMessage.ERROR, - errorDetails: TransactionStoreErrorMessage.SUBGRAPH_TIMEOUT_ERROR - }); - return ctx.onError(); - } - - if (result.value) { - ctx.updateState({ status: TransactionStatusMessage.SUCCESS }); - const newOrderHash = result.value.orderHash; - - // If we have a new order hash, add the "View order" link - if (newOrderHash) { - const newLink = { - link: `/orders/${config.chainId}-${config.orderbook}-${newOrderHash}`, - label: 'View order' - }; - ctx.updateState({ links: [newLink, ...ctx.links] }); - } - - return ctx.onSuccess(); - } - - ctx.updateState({ - status: TransactionStatusMessage.ERROR, - errorDetails: TransactionStoreErrorMessage.SUBGRAPH_FAILED - }); - return ctx.onError(); - }; -} - /** * Creates an indexing function that wraps SDK-based polling logic. * The SDK handles local-DB-first polling followed by subgraph fallback internally, @@ -268,14 +221,9 @@ export class TransactionManager { } ]; - const awaitIndexingFn = createSubgraphIndexingFn({ - chainId, - orderbook, - txHash, - successMessage, - fetchEntityFn: (_chainId: number, orderbook: Address, txHash: Hex) => - raindexClient.getTransaction(orderbook, txHash), - isSuccess: (data) => !!data + const awaitIndexingFn = createSdkIndexingFn({ + call: () => raindexClient.getTransaction(chainId, orderbook, txHash), + isSuccess: (tx) => !!tx }); return this.createTransaction({ @@ -335,14 +283,9 @@ export class TransactionManager { } ]; - const awaitIndexingFn = createSubgraphIndexingFn({ - chainId, - orderbook, - txHash, - successMessage, - fetchEntityFn: (_chainId: number, orderbook: Address, txHash: Hex) => - raindexClient.getTransaction(orderbook, txHash), - isSuccess: (data) => !!data + const awaitIndexingFn = createSdkIndexingFn({ + call: () => raindexClient.getTransaction(chainId, orderbook, txHash), + isSuccess: (tx) => !!tx }); return this.createTransaction({ @@ -459,14 +402,9 @@ export class TransactionManager { } ]; - const awaitIndexingFn = createSubgraphIndexingFn({ - chainId, - orderbook, - txHash, - successMessage, - fetchEntityFn: (_chainId: number, orderbook: `0x${string}`, txHash: `0x${string}`) => - raindexClient.getTransaction(orderbook, txHash), - isSuccess: (data) => !!data + const awaitIndexingFn = createSdkIndexingFn({ + call: () => raindexClient.getTransaction(chainId, orderbook, txHash), + isSuccess: (tx) => !!tx }); return this.createTransaction({ From a7f7e6914778b25d3a22886ca1d3afd5b8f156d7 Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 5 Dec 2025 13:49:44 +0300 Subject: [PATCH 5/7] fix implementation --- .../query/fetch_transaction_by_hash/mod.rs | 9 ++-- .../query/fetch_transaction_by_hash/query.sql | 41 ++++++++++++++----- .../query/fetch_transaction_by_hash.rs | 9 ++-- .../raindex_client/local_db/transactions.rs | 9 ++-- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/crates/common/src/local_db/query/fetch_transaction_by_hash/mod.rs b/crates/common/src/local_db/query/fetch_transaction_by_hash/mod.rs index 69c205b3fd..f163cf6f9b 100644 --- a/crates/common/src/local_db/query/fetch_transaction_by_hash/mod.rs +++ b/crates/common/src/local_db/query/fetch_transaction_by_hash/mod.rs @@ -7,18 +7,18 @@ use serde::{Deserialize, Serialize}; const QUERY_TEMPLATE: &str = include_str!("query.sql"); -/// Transaction info returned from local DB query +/// Transaction info returned from local DB query. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct LocalDbTransaction { pub transaction_hash: B256, pub block_number: u64, pub block_timestamp: u64, - pub owner: Address, + pub sender: Address, } /// Builds a SQL statement to fetch transaction info by transaction hash -/// from the vault_balance_changes table. +/// from the deposits and withdrawals tables. pub fn build_fetch_transaction_by_hash_stmt( ob_id: &OrderbookIdentifier, tx_hash: B256, @@ -47,7 +47,8 @@ mod tests { let stmt = build_fetch_transaction_by_hash_stmt(&ob_id, tx_hash); assert!(stmt.sql.contains("SELECT")); - assert!(stmt.sql.contains("FROM vault_balance_changes")); + assert!(stmt.sql.contains("FROM deposits")); + assert!(stmt.sql.contains("FROM withdrawals")); assert!(stmt.sql.contains("transaction_hash")); assert_eq!(stmt.params.len(), 3); } diff --git a/crates/common/src/local_db/query/fetch_transaction_by_hash/query.sql b/crates/common/src/local_db/query/fetch_transaction_by_hash/query.sql index 6104676a3e..e50635ae74 100644 --- a/crates/common/src/local_db/query/fetch_transaction_by_hash/query.sql +++ b/crates/common/src/local_db/query/fetch_transaction_by_hash/query.sql @@ -3,17 +3,38 @@ WITH params AS ( ?1 AS chain_id, ?2 AS orderbook_address, ?3 AS transaction_hash +), +combined AS ( + SELECT + d.transaction_hash, + d.block_number, + d.block_timestamp, + d.sender, + d.log_index + FROM deposits d + JOIN params p + ON p.chain_id = d.chain_id + AND p.orderbook_address = d.orderbook_address + AND p.transaction_hash = d.transaction_hash + UNION ALL + SELECT + w.transaction_hash, + w.block_number, + w.block_timestamp, + w.sender, + w.log_index + FROM withdrawals w + JOIN params p + ON p.chain_id = w.chain_id + AND p.orderbook_address = w.orderbook_address + AND p.transaction_hash = w.transaction_hash ) SELECT - vbc.transaction_hash AS transactionHash, - vbc.block_number AS blockNumber, - vbc.block_timestamp AS blockTimestamp, - vbc.owner -FROM vault_balance_changes vbc -JOIN params p - ON p.chain_id = vbc.chain_id - AND p.orderbook_address = vbc.orderbook_address - AND p.transaction_hash = vbc.transaction_hash -ORDER BY vbc.log_index ASC + transaction_hash AS transactionHash, + block_number AS blockNumber, + block_timestamp AS blockTimestamp, + sender +FROM combined +ORDER BY log_index ASC LIMIT 1; diff --git a/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs b/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs index 66a86d2a15..5e5375bf02 100644 --- a/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs +++ b/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs @@ -23,6 +23,7 @@ mod wasm_tests { 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() { @@ -33,7 +34,7 @@ mod wasm_tests { let store = Rc::new(RefCell::new(( String::new(), - wasm_bindgen::JsValue::UNDEFINED, + JsValue::UNDEFINED, ))); let callback = create_sql_capturing_callback("[]", store.clone()); let exec = JsCallbackExecutor::from_ref(&callback); @@ -52,7 +53,7 @@ mod wasm_tests { async fn wrapper_returns_rows_when_present() { let tx_hash = b256!("0x0000000000000000000000000000000000000000000000000000000000000abc"); let orderbook = address!("0x5151515151515151515151515151515151515151"); - let owner = address!("0x1111111111111111111111111111111111111111"); + let sender = address!("0x1111111111111111111111111111111111111111"); let expected_stmt = build_fetch_transaction_by_hash_stmt(&OrderbookIdentifier::new(1, orderbook), tx_hash); @@ -61,9 +62,9 @@ mod wasm_tests { "transactionHash":"{}", "blockNumber":100, "blockTimestamp":999, - "owner":"{}" + "sender":"{}" }}]"#, - tx_hash, owner + tx_hash, sender ); let store = Rc::new(RefCell::new(( diff --git a/crates/common/src/raindex_client/local_db/transactions.rs b/crates/common/src/raindex_client/local_db/transactions.rs index 312775814d..ca41839a2f 100644 --- a/crates/common/src/raindex_client/local_db/transactions.rs +++ b/crates/common/src/raindex_client/local_db/transactions.rs @@ -30,7 +30,7 @@ impl<'a> LocalDbTransactions<'a> { if let Some(local_tx) = results.into_iter().next() { let tx = RaindexTransaction::from_local_parts( local_tx.transaction_hash, - local_tx.owner, + local_tx.sender, local_tx.block_number, local_tx.block_timestamp, )?; @@ -53,9 +53,6 @@ mod tests { use crate::raindex_client::local_db::LocalDb; use alloy::primitives::{address, b256}; use serde_json::json; - use std::cell::RefCell; - use std::rc::Rc; - use wasm_bindgen::prelude::*; use wasm_bindgen_test::wasm_bindgen_test; use wasm_bindgen_utils::prelude::*; @@ -83,14 +80,14 @@ mod tests { async fn test_get_by_tx_hash_returns_transaction_when_found() { let tx_hash = b256!("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"); - let owner = address!("0x1111111111111111111111111111111111111111"); + let sender = address!("0x1111111111111111111111111111111111111111"); let orderbook = address!("0x2222222222222222222222222222222222222222"); let tx_json = json!([{ "transactionHash": tx_hash.to_string(), "blockNumber": 12345, "blockTimestamp": 1700000000, - "owner": owner.to_string() + "sender": sender.to_string() }]); let callback = create_mock_callback(&tx_json.to_string()); From cb712589102a62b6c6375a67f4e9822fea951b0b Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Fri, 5 Dec 2025 13:52:50 +0300 Subject: [PATCH 6/7] formatting --- .../local_db/query/fetch_transaction_by_hash.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs b/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs index 5e5375bf02..959ebaa06f 100644 --- a/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs +++ b/crates/common/src/raindex_client/local_db/query/fetch_transaction_by_hash.rs @@ -32,10 +32,7 @@ mod wasm_tests { let expected_stmt = build_fetch_transaction_by_hash_stmt(&OrderbookIdentifier::new(1, orderbook), tx_hash); - let store = Rc::new(RefCell::new(( - String::new(), - JsValue::UNDEFINED, - ))); + let store = Rc::new(RefCell::new((String::new(), JsValue::UNDEFINED))); let callback = create_sql_capturing_callback("[]", store.clone()); let exec = JsCallbackExecutor::from_ref(&callback); From ab65cbd604df387f52a297cf04791bd309bd8bee Mon Sep 17 00:00:00 2001 From: Arda Nakisci Date: Wed, 28 Jan 2026 09:25:23 +0300 Subject: [PATCH 7/7] Fix missing chainId argument in getTransaction test --- packages/orderbook/test/js_api/raindexClient.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orderbook/test/js_api/raindexClient.test.ts b/packages/orderbook/test/js_api/raindexClient.test.ts index b6cf62d7ea..402affe272 100644 --- a/packages/orderbook/test/js_api/raindexClient.test.ts +++ b/packages/orderbook/test/js_api/raindexClient.test.ts @@ -1983,7 +1983,7 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Raindex Client', async f const raindexClient = extractWasmEncodedData(RaindexClient.new([YAML])); const result = extractWasmEncodedData( - await raindexClient.getTransaction(CHAIN_ID_1_ORDERBOOK_ADDRESS, BYTES32_0123) + await raindexClient.getTransaction(1, CHAIN_ID_1_ORDERBOOK_ADDRESS, BYTES32_0123) ); assert.equal(result.id, transaction.id); assert.equal(result.from, transaction.from);