1use async_trait::async_trait;
8use eyre::Result;
9use soroban_rs::stellar_rpc_client::Client;
10use soroban_rs::stellar_rpc_client::{
11 Error as StellarClientError, EventStart, EventType, GetEventsResponse, GetLatestLedgerResponse,
12 GetLedgerEntriesResponse, GetNetworkResponse, GetTransactionResponse,
13 GetTransactionResponseRaw, GetTransactionsRequest, GetTransactionsResponse,
14 SendTransactionResponse, SimulateTransactionResponse,
15};
16use soroban_rs::xdr::{
17 AccountEntry, ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp,
18 LedgerKey, Limits, MuxedAccount, Operation, OperationBody, ReadXdr, ScAddress, ScSymbol, ScVal,
19 SequenceNumber, Transaction, TransactionEnvelope, TransactionV1Envelope, Uint256, VecM,
20 WriteXdr,
21};
22#[cfg(test)]
23use soroban_rs::xdr::{AccountId, LedgerKeyAccount, PublicKey};
24use soroban_rs::SorobanTransactionResponse;
25use std::sync::atomic::{AtomicU64, Ordering};
26
27#[cfg(test)]
28use mockall::automock;
29
30use once_cell::sync::Lazy;
31
32use crate::models::{JsonRpcId, RpcConfig};
33use crate::services::client_cache::SyncClientCache;
34use crate::services::provider::is_retriable_error;
35use crate::services::provider::retry::retry_rpc_call;
36use crate::services::provider::rpc_selector::RpcSelector;
37use crate::services::provider::should_mark_provider_failed;
38use crate::services::provider::RetryConfig;
39use crate::services::provider::{ProviderConfig, ProviderError};
40use crate::utils::validate_safe_url;
43use reqwest::Client as ReqwestClient;
44use std::sync::Arc;
45use std::time::Duration;
46
47fn generate_unique_rpc_id() -> u64 {
56 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
57 NEXT_ID.fetch_add(1, Ordering::Relaxed)
58}
59
60static STELLAR_RPC_CLIENT_CACHE: Lazy<SyncClientCache<String, Client>> =
63 Lazy::new(SyncClientCache::new);
64
65fn categorize_stellar_error_with_context(
84 err: StellarClientError,
85 context: Option<&str>,
86) -> ProviderError {
87 let add_context = |msg: String| -> String {
88 match context {
89 Some(ctx) => format!("{ctx}: {msg}"),
90 None => msg,
91 }
92 };
93 match err {
94 StellarClientError::TransactionSubmissionTimeout => ProviderError::Timeout,
96
97 StellarClientError::InvalidAddress(decode_err) => ProviderError::InvalidAddress(
99 add_context(format!("Invalid Stellar address: {decode_err}")),
100 ),
101
102 StellarClientError::Xdr(xdr_err) => {
104 ProviderError::Other(add_context(format!("XDR processing error: {xdr_err}")))
105 }
106
107 StellarClientError::Serde(serde_err) => {
109 ProviderError::Other(add_context(format!("JSON parsing error: {serde_err}")))
110 }
111
112 StellarClientError::InvalidRpcUrl(uri_err) => {
114 ProviderError::NetworkConfiguration(add_context(format!("Invalid RPC URL: {uri_err}")))
115 }
116 StellarClientError::InvalidRpcUrlFromUriParts(uri_err) => {
117 ProviderError::NetworkConfiguration(add_context(format!(
118 "Invalid RPC URL parts: {uri_err}"
119 )))
120 }
121 StellarClientError::InvalidUrl(url) => {
122 ProviderError::NetworkConfiguration(add_context(format!("Invalid URL: {url}")))
123 }
124
125 StellarClientError::InvalidNetworkPassphrase { expected, server } => {
127 ProviderError::NetworkConfiguration(add_context(format!(
128 "Network passphrase mismatch: expected {expected:?}, server returned {server:?}"
129 )))
130 }
131
132 StellarClientError::JsonRpc(jsonrpsee_err) => {
134 match jsonrpsee_err {
135 jsonrpsee_core::error::Error::Call(err_obj) => {
137 let code = err_obj.code() as i64;
138 let message = add_context(err_obj.message().to_string());
139 ProviderError::RpcErrorCode { code, message }
140 }
141
142 jsonrpsee_core::error::Error::RequestTimeout => ProviderError::Timeout,
144
145 jsonrpsee_core::error::Error::Transport(transport_err) => {
147 let mut source = transport_err.source();
149 while let Some(s) = source {
150 if let Some(reqwest_err) = s.downcast_ref::<reqwest::Error>() {
151 return ProviderError::from(reqwest_err);
152 }
153 source = s.source();
154 }
155
156 ProviderError::TransportError(add_context(format!(
157 "Transport error: {transport_err}"
158 )))
159 }
160 other => ProviderError::Other(add_context(format!("JSON-RPC error: {other}"))),
162 }
163 }
164 StellarClientError::InvalidResponse => {
166 ProviderError::Other(add_context(
168 "Invalid response from Stellar RPC server".to_string(),
169 ))
170 }
171 StellarClientError::MissingResult => {
172 ProviderError::Other(add_context("Missing result in RPC response".to_string()))
173 }
174 StellarClientError::MissingError => ProviderError::Other(add_context(
175 "Failed to read error from RPC response".to_string(),
176 )),
177
178 StellarClientError::TransactionFailed(msg) => {
180 ProviderError::Other(add_context(format!("Transaction failed: {msg}")))
181 }
182 StellarClientError::TransactionSubmissionFailed(msg) => {
183 ProviderError::Other(add_context(format!("Transaction submission failed: {msg}")))
184 }
185 StellarClientError::TransactionSimulationFailed(msg) => {
186 ProviderError::Other(add_context(format!("Transaction simulation failed: {msg}")))
187 }
188 StellarClientError::UnexpectedTransactionStatus(status) => ProviderError::Other(
189 add_context(format!("Unexpected transaction status: {status}")),
190 ),
191
192 StellarClientError::NotFound(resource, id) => {
194 ProviderError::Other(add_context(format!("{resource} not found: {id}")))
195 }
196
197 StellarClientError::InvalidCursor => {
199 ProviderError::Other(add_context("Invalid cursor".to_string()))
200 }
201 StellarClientError::UnexpectedSimulateTransactionResultSize { length } => {
202 ProviderError::Other(add_context(format!(
203 "Unexpected simulate transaction result size: {length}"
204 )))
205 }
206 StellarClientError::UnexpectedOperationCount { count } => {
207 ProviderError::Other(add_context(format!("Unexpected operation count: {count}")))
208 }
209 StellarClientError::UnsupportedOperationType => {
210 ProviderError::Other(add_context("Unsupported operation type".to_string()))
211 }
212 StellarClientError::UnexpectedContractCodeDataType(data) => ProviderError::Other(
213 add_context(format!("Unexpected contract code data type: {data:?}")),
214 ),
215 StellarClientError::UnexpectedContractInstance(val) => ProviderError::Other(add_context(
216 format!("Unexpected contract instance: {val:?}"),
217 )),
218 StellarClientError::LargeFee(fee) => {
219 ProviderError::Other(add_context(format!("Fee too large: {fee}")))
220 }
221 StellarClientError::CannotAuthorizeRawTransaction => {
222 ProviderError::Other(add_context("Cannot authorize raw transaction".to_string()))
223 }
224 StellarClientError::MissingOp => {
225 ProviderError::Other(add_context("Missing operation in transaction".to_string()))
226 }
227 StellarClientError::MissingSignerForAddress { address } => ProviderError::Other(
228 add_context(format!("Missing signer for address: {address}")),
229 ),
230
231 #[allow(deprecated)]
233 StellarClientError::UnexpectedToken(entry) => {
234 ProviderError::Other(add_context(format!("Unexpected token: {entry:?}")))
235 }
236 }
237}
238
239fn normalize_url_for_log(url: &str) -> String {
245 let mut s = url.to_string();
247 if let Some(q) = s.find('?') {
248 s.truncate(q);
249 }
250 if let Some(h) = s.find('#') {
251 s.truncate(h);
252 }
253
254 if let Some(scheme_pos) = s.find("://") {
256 let start = scheme_pos + 3;
257 if let Some(at_pos) = s[start..].find('@') {
258 let after = &s[start + at_pos + 1..];
259 let prefix = &s[..start];
260 s = format!("{prefix}<redacted>@{after}");
261 }
262 }
263
264 s
265}
266#[derive(Debug, Clone)]
267pub struct GetEventsRequest {
268 pub start: EventStart,
269 pub event_type: Option<EventType>,
270 pub contract_ids: Vec<String>,
271 pub topics: Vec<Vec<String>>,
272 pub limit: Option<usize>,
273}
274
275#[derive(Clone, Debug)]
276pub struct StellarProvider {
277 selector: RpcSelector,
279 timeout_seconds: Duration,
281 retry_config: RetryConfig,
283}
284
285#[async_trait]
286#[cfg_attr(test, automock)]
287#[allow(dead_code)]
288pub trait StellarProviderTrait: Send + Sync {
289 fn get_configs(&self) -> Vec<RpcConfig>;
290 async fn get_account(&self, account_id: &str) -> Result<AccountEntry, ProviderError>;
291 async fn simulate_transaction_envelope(
292 &self,
293 tx_envelope: &TransactionEnvelope,
294 ) -> Result<SimulateTransactionResponse, ProviderError>;
295 async fn send_transaction_polling(
296 &self,
297 tx_envelope: &TransactionEnvelope,
298 ) -> Result<SorobanTransactionResponse, ProviderError>;
299 async fn get_network(&self) -> Result<GetNetworkResponse, ProviderError>;
300 async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, ProviderError>;
301 async fn send_transaction(
302 &self,
303 tx_envelope: &TransactionEnvelope,
304 ) -> Result<Hash, ProviderError>;
305 async fn send_transaction_with_status(
322 &self,
323 tx_envelope: &TransactionEnvelope,
324 ) -> Result<SendTransactionResponse, ProviderError>;
325 async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, ProviderError>;
326 async fn get_transactions(
327 &self,
328 request: GetTransactionsRequest,
329 ) -> Result<GetTransactionsResponse, ProviderError>;
330 async fn get_ledger_entries(
331 &self,
332 keys: &[LedgerKey],
333 ) -> Result<GetLedgerEntriesResponse, ProviderError>;
334 async fn get_events(
335 &self,
336 request: GetEventsRequest,
337 ) -> Result<GetEventsResponse, ProviderError>;
338 async fn raw_request_dyn(
339 &self,
340 method: &str,
341 params: serde_json::Value,
342 id: Option<JsonRpcId>,
343 ) -> Result<serde_json::Value, ProviderError>;
344 async fn call_contract(
357 &self,
358 contract_address: &str,
359 function_name: &ScSymbol,
360 args: Vec<ScVal>,
361 ) -> Result<ScVal, ProviderError>;
362}
363
364impl StellarProvider {
365 pub fn new(config: ProviderConfig) -> Result<Self, ProviderError> {
367 if config.rpc_configs.is_empty() {
368 return Err(ProviderError::NetworkConfiguration(
369 "No RPC configurations provided for StellarProvider".to_string(),
370 ));
371 }
372
373 RpcConfig::validate_list(&config.rpc_configs)
374 .map_err(|e| ProviderError::NetworkConfiguration(e.to_string()))?;
375
376 let mut rpc_configs = config.rpc_configs;
377 rpc_configs.retain(|config| config.get_weight() > 0);
378
379 if rpc_configs.is_empty() {
380 return Err(ProviderError::NetworkConfiguration(
381 "No active RPC configurations provided (all weights are 0 or list was empty after filtering)".to_string(),
382 ));
383 }
384
385 let selector = RpcSelector::new(
386 rpc_configs,
387 config.failure_threshold,
388 config.pause_duration_secs,
389 config.failure_expiration_secs,
390 )
391 .map_err(|e| {
392 ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {e}"))
393 })?;
394
395 let retry_config = RetryConfig::from_env();
396
397 Ok(Self {
398 selector,
399 timeout_seconds: Duration::from_secs(config.timeout_seconds),
400 retry_config,
401 })
402 }
403
404 pub fn get_configs(&self) -> Vec<RpcConfig> {
409 self.selector.get_configs()
410 }
411
412 fn initialize_provider(&self, url: &str) -> Result<Arc<Client>, ProviderError> {
415 let allowed_hosts = crate::config::ServerConfig::get_rpc_allowed_hosts();
416 let block_private_ips = crate::config::ServerConfig::get_rpc_block_private_ips();
417 validate_safe_url(url, &allowed_hosts, block_private_ips).map_err(|e| {
418 ProviderError::NetworkConfiguration(format!("RPC URL security validation failed: {e}"))
419 })?;
420
421 STELLAR_RPC_CLIENT_CACHE.get_or_try_init(url.to_string(), || {
422 Client::new(url).map_err(|e| {
423 let safe_url = crate::utils::mask_url(url);
424 ProviderError::NetworkConfiguration(format!(
425 "Failed to create Stellar RPC client: {e} - URL: '{safe_url}'"
426 ))
427 })
428 })
429 }
430
431 fn initialize_raw_provider(&self, url: &str) -> Result<ReqwestClient, ProviderError> {
434 let allowed_hosts = crate::config::ServerConfig::get_rpc_allowed_hosts();
435 let block_private_ips = crate::config::ServerConfig::get_rpc_block_private_ips();
436 validate_safe_url(url, &allowed_hosts, block_private_ips).map_err(|e| {
437 ProviderError::NetworkConfiguration(format!("RPC URL security validation failed: {e}"))
438 })?;
439
440 super::get_shared_rpc_http_client()
441 }
442
443 async fn retry_rpc_call<T, F, Fut>(
445 &self,
446 operation_name: &str,
447 operation: F,
448 ) -> Result<T, ProviderError>
449 where
450 F: Fn(Arc<Client>) -> Fut,
451 Fut: std::future::Future<Output = Result<T, ProviderError>>,
452 {
453 let provider_url_raw = match self.selector.get_current_url() {
454 Ok(url) => url,
455 Err(e) => {
456 return Err(ProviderError::NetworkConfiguration(format!(
457 "No RPC URL available for StellarProvider: {e}"
458 )));
459 }
460 };
461 let provider_url = normalize_url_for_log(&provider_url_raw);
462
463 tracing::debug!(
464 "Starting Stellar RPC operation '{}' with timeout: {}s, provider_url: {}",
465 operation_name,
466 self.timeout_seconds.as_secs(),
467 provider_url
468 );
469
470 retry_rpc_call(
471 &self.selector,
472 operation_name,
473 is_retriable_error,
474 should_mark_provider_failed,
475 |url| self.initialize_provider(url),
476 operation,
477 Some(self.retry_config.clone()),
478 )
479 .await
480 }
481
482 async fn retry_raw_request(
484 &self,
485 operation_name: &str,
486 request: serde_json::Value,
487 ) -> Result<serde_json::Value, ProviderError> {
488 let provider_url_raw = match self.selector.get_current_url() {
489 Ok(url) => url,
490 Err(e) => {
491 return Err(ProviderError::NetworkConfiguration(format!(
492 "No RPC URL available for StellarProvider: {e}"
493 )));
494 }
495 };
496 let provider_url = normalize_url_for_log(&provider_url_raw);
497
498 tracing::debug!(
499 "Starting raw RPC operation '{}' with timeout: {}s, provider_url: {}",
500 operation_name,
501 self.timeout_seconds.as_secs(),
502 provider_url
503 );
504
505 let request_clone = request.clone();
506 retry_rpc_call(
507 &self.selector,
508 operation_name,
509 is_retriable_error,
510 should_mark_provider_failed,
511 |url| {
512 self.initialize_raw_provider(url)
514 .map(|client| (url.to_string(), client))
515 },
516 |(url, client): (String, ReqwestClient)| {
517 let request_for_call = request_clone.clone();
518 async move {
519 let response = client
520 .post(&url)
521 .json(&request_for_call)
522 .timeout(self.timeout_seconds)
524 .send()
525 .await
526 .map_err(ProviderError::from)?;
527
528 let status = response.status();
534 if !status.is_success() {
535 let body = response.text().await.unwrap_or_default();
536 return Err(http_status_error_to_provider_error(status, body));
537 }
538
539 let json_response: serde_json::Value =
540 response.json().await.map_err(ProviderError::from)?;
541
542 if let Some(error) = json_response.get("error").filter(|e| !e.is_null()) {
547 return Err(json_rpc_error_to_provider_error(error));
548 }
549
550 Ok(json_response)
551 }
552 },
553 Some(self.retry_config.clone()),
554 )
555 .await
556 }
557
558 async fn raw_json_rpc_request(
567 &self,
568 operation_name: &str,
569 method: &str,
570 params: serde_json::Value,
571 id: Option<JsonRpcId>,
572 ) -> Result<serde_json::Value, ProviderError> {
573 let id_value = match id {
574 Some(id) => serde_json::to_value(id)
575 .map_err(|e| ProviderError::Other(format!("Failed to serialize id: {e}")))?,
576 None => serde_json::json!(generate_unique_rpc_id()),
577 };
578
579 let request = serde_json::json!({
580 "jsonrpc": "2.0",
581 "id": id_value,
582 "method": method,
583 "params": params,
584 });
585
586 let response = self.retry_raw_request(operation_name, request).await?;
587
588 response
590 .get("result")
591 .cloned()
592 .ok_or_else(|| ProviderError::Other("No result field in JSON-RPC response".to_string()))
593 }
594}
595
596fn http_status_error_to_provider_error(status: reqwest::StatusCode, body: String) -> ProviderError {
601 match status.as_u16() {
602 429 => ProviderError::RateLimited,
603 502 => ProviderError::BadGateway,
604 code => {
605 let detail = if body.trim().is_empty() {
606 status.to_string()
607 } else {
608 body
609 };
610 ProviderError::RequestError {
611 error: detail,
612 status_code: code,
613 }
614 }
615 }
616}
617
618fn json_rpc_error_to_provider_error(error: &serde_json::Value) -> ProviderError {
621 if let Some(code) = error.get("code").and_then(|c| c.as_i64()) {
622 return ProviderError::RpcErrorCode {
623 code,
624 message: error
625 .get("message")
626 .and_then(|m| m.as_str())
627 .unwrap_or("Unknown error")
628 .to_string(),
629 };
630 }
631 ProviderError::Other(format!("JSON-RPC error: {error}"))
632}
633
634#[async_trait]
635impl StellarProviderTrait for StellarProvider {
636 fn get_configs(&self) -> Vec<RpcConfig> {
637 self.get_configs()
638 }
639
640 async fn get_account(&self, account_id: &str) -> Result<AccountEntry, ProviderError> {
641 let account_id = Arc::new(account_id.to_string());
642
643 self.retry_rpc_call("get_account", move |client| {
644 let account_id = Arc::clone(&account_id);
645 async move {
646 client.get_account(&account_id).await.map_err(|e| {
647 categorize_stellar_error_with_context(e, Some("Failed to get account"))
648 })
649 }
650 })
651 .await
652 }
653
654 async fn simulate_transaction_envelope(
655 &self,
656 tx_envelope: &TransactionEnvelope,
657 ) -> Result<SimulateTransactionResponse, ProviderError> {
658 let tx_envelope = Arc::new(tx_envelope.clone());
659
660 self.retry_rpc_call("simulate_transaction_envelope", move |client| {
661 let tx_envelope = Arc::clone(&tx_envelope);
662 async move {
663 client
664 .simulate_transaction_envelope(&tx_envelope, None)
665 .await
666 .map_err(|e| {
667 categorize_stellar_error_with_context(
668 e,
669 Some("Failed to simulate transaction"),
670 )
671 })
672 }
673 })
674 .await
675 }
676
677 async fn send_transaction_polling(
678 &self,
679 tx_envelope: &TransactionEnvelope,
680 ) -> Result<SorobanTransactionResponse, ProviderError> {
681 let tx_envelope = Arc::new(tx_envelope.clone());
682
683 self.retry_rpc_call("send_transaction_polling", move |client| {
684 let tx_envelope = Arc::clone(&tx_envelope);
685 async move {
686 client
687 .send_transaction_polling(&tx_envelope)
688 .await
689 .map(SorobanTransactionResponse::from)
690 .map_err(|e| {
691 categorize_stellar_error_with_context(
692 e,
693 Some("Failed to send transaction (polling)"),
694 )
695 })
696 }
697 })
698 .await
699 }
700
701 async fn get_network(&self) -> Result<GetNetworkResponse, ProviderError> {
702 self.retry_rpc_call("get_network", |client| async move {
703 client.get_network().await.map_err(|e| {
704 categorize_stellar_error_with_context(e, Some("Failed to get network"))
705 })
706 })
707 .await
708 }
709
710 async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, ProviderError> {
711 self.retry_rpc_call("get_latest_ledger", |client| async move {
712 client.get_latest_ledger().await.map_err(|e| {
713 categorize_stellar_error_with_context(e, Some("Failed to get latest ledger"))
714 })
715 })
716 .await
717 }
718
719 async fn send_transaction(
720 &self,
721 tx_envelope: &TransactionEnvelope,
722 ) -> Result<Hash, ProviderError> {
723 let tx_envelope = Arc::new(tx_envelope.clone());
724
725 self.retry_rpc_call("send_transaction", move |client| {
726 let tx_envelope = Arc::clone(&tx_envelope);
727 async move {
728 client.send_transaction(&tx_envelope).await.map_err(|e| {
729 categorize_stellar_error_with_context(e, Some("Failed to send transaction"))
730 })
731 }
732 })
733 .await
734 }
735
736 async fn send_transaction_with_status(
737 &self,
738 tx_envelope: &TransactionEnvelope,
739 ) -> Result<SendTransactionResponse, ProviderError> {
740 let tx_xdr = tx_envelope
742 .to_xdr_base64(Limits::none())
743 .map_err(|e| ProviderError::Other(format!("Failed to encode transaction XDR: {e}")))?;
744
745 let params = serde_json::json!({
747 "transaction": tx_xdr
748 });
749
750 let result = self
751 .raw_request_dyn("sendTransaction", params, None)
752 .await?;
753
754 serde_json::from_value(result).map_err(|e| {
756 ProviderError::Other(format!(
757 "Failed to deserialize SendTransactionResponse: {e}"
758 ))
759 })
760 }
761
762 async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, ProviderError> {
768 let params = serde_json::json!({ "hash": tx_id });
769
770 let result = self
771 .raw_json_rpc_request("get_transaction", "getTransaction", params, None)
772 .await?;
773
774 let raw: GetTransactionResponseRaw = serde_json::from_value(result).map_err(|e| {
775 ProviderError::Other(format!("Failed to deserialize GetTransactionResponse: {e}"))
776 })?;
777
778 raw.try_into().map_err(|e: soroban_rs::xdr::Error| {
779 ProviderError::Other(format!("Failed to decode getTransaction XDR: {e}"))
780 })
781 }
782
783 async fn get_transactions(
784 &self,
785 request: GetTransactionsRequest,
786 ) -> Result<GetTransactionsResponse, ProviderError> {
787 let request = Arc::new(request);
788
789 self.retry_rpc_call("get_transactions", move |client| {
790 let request = Arc::clone(&request);
791 async move {
792 client
793 .get_transactions((*request).clone())
794 .await
795 .map_err(|e| {
796 categorize_stellar_error_with_context(e, Some("Failed to get transactions"))
797 })
798 }
799 })
800 .await
801 }
802
803 async fn get_ledger_entries(
804 &self,
805 keys: &[LedgerKey],
806 ) -> Result<GetLedgerEntriesResponse, ProviderError> {
807 let keys = Arc::new(keys.to_vec());
808
809 self.retry_rpc_call("get_ledger_entries", move |client| {
810 let keys = Arc::clone(&keys);
811 async move {
812 client.get_ledger_entries(&keys).await.map_err(|e| {
813 categorize_stellar_error_with_context(e, Some("Failed to get ledger entries"))
814 })
815 }
816 })
817 .await
818 }
819
820 async fn get_events(
821 &self,
822 request: GetEventsRequest,
823 ) -> Result<GetEventsResponse, ProviderError> {
824 let request = Arc::new(request);
825
826 self.retry_rpc_call("get_events", move |client| {
827 let request = Arc::clone(&request);
828 async move {
829 client
830 .get_events(
831 request.start.clone(),
832 request.event_type,
833 &request.contract_ids,
834 &request.topics,
835 request.limit,
836 )
837 .await
838 .map_err(|e| {
839 categorize_stellar_error_with_context(e, Some("Failed to get events"))
840 })
841 }
842 })
843 .await
844 }
845
846 async fn raw_request_dyn(
847 &self,
848 method: &str,
849 params: serde_json::Value,
850 id: Option<JsonRpcId>,
851 ) -> Result<serde_json::Value, ProviderError> {
852 self.raw_json_rpc_request("raw_request_dyn", method, params, id)
853 .await
854 }
855
856 async fn call_contract(
857 &self,
858 contract_address: &str,
859 function_name: &ScSymbol,
860 args: Vec<ScVal>,
861 ) -> Result<ScVal, ProviderError> {
862 let contract = stellar_strkey::Contract::from_string(contract_address)
864 .map_err(|e| ProviderError::Other(format!("Invalid contract address: {e}")))?;
865 let contract_addr = ScAddress::Contract(ContractId(Hash(contract.0)));
866
867 let args_vec = VecM::try_from(args)
869 .map_err(|e| ProviderError::Other(format!("Failed to convert arguments: {e:?}")))?;
870
871 let host_function = HostFunction::InvokeContract(InvokeContractArgs {
873 contract_address: contract_addr,
874 function_name: function_name.clone(),
875 args: args_vec,
876 });
877
878 let operation = Operation {
879 source_account: None,
880 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
881 host_function,
882 auth: VecM::try_from(vec![]).unwrap(),
883 }),
884 };
885
886 let dummy_account = MuxedAccount::Ed25519(Uint256([0u8; 32]));
901 let operations: VecM<Operation, 100> = vec![operation].try_into().map_err(|e| {
902 ProviderError::Other(format!("Failed to create operations vector: {e:?}"))
903 })?;
904
905 let tx = Transaction {
906 source_account: dummy_account,
907 fee: 100,
908 seq_num: SequenceNumber(0),
909 cond: soroban_rs::xdr::Preconditions::None,
910 memo: soroban_rs::xdr::Memo::None,
911 operations,
912 ext: soroban_rs::xdr::TransactionExt::V0,
913 };
914
915 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
916 tx,
917 signatures: VecM::try_from(vec![]).unwrap(),
918 });
919
920 let sim_response = self.simulate_transaction_envelope(&envelope).await?;
922
923 if let Some(error) = sim_response.error {
925 return Err(ProviderError::Other(format!(
926 "Contract invocation simulation failed: {error}",
927 )));
928 }
929
930 if sim_response.results.is_empty() {
932 return Err(ProviderError::Other(
933 "Simulation returned no results".to_string(),
934 ));
935 }
936
937 let result_xdr = &sim_response.results[0].xdr;
939 ScVal::from_xdr_base64(result_xdr, Limits::none()).map_err(|e| {
940 ProviderError::Other(format!("Failed to parse simulation result XDR: {e}"))
941 })
942 }
943}
944
945#[cfg(test)]
946mod stellar_rpc_tests {
947 use super::*;
948 use crate::services::provider::stellar::{
949 GetEventsRequest, StellarProvider, StellarProviderTrait,
950 };
951 use futures::FutureExt;
952 use lazy_static::lazy_static;
953 use mockall::predicate as p;
954 use soroban_rs::stellar_rpc_client::{
955 EventStart, GetEventsResponse, GetLatestLedgerResponse, GetLedgerEntriesResponse,
956 GetNetworkResponse, GetTransactionEvents, GetTransactionResponse, GetTransactionsRequest,
957 GetTransactionsResponse, SimulateTransactionResponse,
958 };
959 use soroban_rs::xdr::{
960 AccountEntryExt, Hash, LedgerKey, OperationResult, String32, Thresholds,
961 TransactionEnvelope, TransactionResult, TransactionResultExt, TransactionResultResult,
962 VecM,
963 };
964 use soroban_rs::{create_mock_set_options_tx_envelope, SorobanTransactionResponse};
965 use std::str::FromStr;
966 use std::sync::Mutex;
967
968 lazy_static! {
969 static ref STELLAR_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
970 }
971
972 struct StellarTestEnvGuard {
973 _mutex_guard: std::sync::MutexGuard<'static, ()>,
974 }
975
976 impl StellarTestEnvGuard {
977 fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
978 std::env::set_var(
979 "API_KEY",
980 "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
981 );
982 std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
983 std::env::set_var("PROVIDER_MAX_RETRIES", "1");
985 std::env::set_var("PROVIDER_MAX_FAILOVERS", "0");
986 std::env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "0");
987 std::env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "0");
988
989 Self {
990 _mutex_guard: mutex_guard,
991 }
992 }
993 }
994
995 impl Drop for StellarTestEnvGuard {
996 fn drop(&mut self) {
997 std::env::remove_var("API_KEY");
998 std::env::remove_var("REDIS_URL");
999 std::env::remove_var("PROVIDER_MAX_RETRIES");
1000 std::env::remove_var("PROVIDER_MAX_FAILOVERS");
1001 std::env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
1002 std::env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
1003 }
1004 }
1005
1006 fn setup_test_env() -> StellarTestEnvGuard {
1008 let guard = STELLAR_TEST_ENV_MUTEX
1009 .lock()
1010 .unwrap_or_else(|e| e.into_inner());
1011 StellarTestEnvGuard::new(guard)
1012 }
1013
1014 fn dummy_hash() -> Hash {
1015 Hash([0u8; 32])
1016 }
1017
1018 fn dummy_get_network_response() -> GetNetworkResponse {
1019 GetNetworkResponse {
1020 friendbot_url: Some("https://friendbot.testnet.stellar.org/".into()),
1021 passphrase: "Test SDF Network ; September 2015".into(),
1022 protocol_version: 20,
1023 }
1024 }
1025
1026 fn dummy_get_latest_ledger_response() -> GetLatestLedgerResponse {
1027 GetLatestLedgerResponse {
1028 id: "c73c5eac58a441d4eb733c35253ae85f783e018f7be5ef974258fed067aabb36".into(),
1029 protocol_version: 20,
1030 sequence: 2_539_605,
1031 }
1032 }
1033
1034 fn dummy_simulate() -> SimulateTransactionResponse {
1035 SimulateTransactionResponse {
1036 min_resource_fee: 100,
1037 transaction_data: "test".to_string(),
1038 ..Default::default()
1039 }
1040 }
1041
1042 fn create_success_tx_result() -> TransactionResult {
1043 let empty_vec: Vec<OperationResult> = Vec::new();
1045 let op_results = empty_vec.try_into().unwrap_or_default();
1046
1047 TransactionResult {
1048 fee_charged: 100,
1049 result: TransactionResultResult::TxSuccess(op_results),
1050 ext: TransactionResultExt::V0,
1051 }
1052 }
1053
1054 fn dummy_get_transaction_response() -> GetTransactionResponse {
1055 GetTransactionResponse {
1056 status: "SUCCESS".to_string(),
1057 envelope: None,
1058 result: Some(create_success_tx_result()),
1059 result_meta: None,
1060 events: GetTransactionEvents {
1061 contract_events: vec![],
1062 diagnostic_events: vec![],
1063 transaction_events: vec![],
1064 },
1065 ledger: None,
1066 }
1067 }
1068
1069 fn dummy_soroban_tx() -> SorobanTransactionResponse {
1070 SorobanTransactionResponse {
1071 response: dummy_get_transaction_response(),
1072 }
1073 }
1074
1075 fn dummy_get_transactions_response() -> GetTransactionsResponse {
1076 GetTransactionsResponse {
1077 transactions: vec![],
1078 latest_ledger: 0,
1079 latest_ledger_close_time: 0,
1080 oldest_ledger: 0,
1081 oldest_ledger_close_time: 0,
1082 cursor: 0,
1083 }
1084 }
1085
1086 fn dummy_get_ledger_entries_response() -> GetLedgerEntriesResponse {
1087 GetLedgerEntriesResponse {
1088 entries: None,
1089 latest_ledger: 0,
1090 }
1091 }
1092
1093 fn dummy_get_events_response() -> GetEventsResponse {
1094 GetEventsResponse {
1095 events: vec![],
1096 latest_ledger: 0,
1097 latest_ledger_close_time: "0".to_string(),
1098 oldest_ledger: 0,
1099 oldest_ledger_close_time: "0".to_string(),
1100 cursor: "0".to_string(),
1101 }
1102 }
1103
1104 fn dummy_transaction_envelope() -> TransactionEnvelope {
1105 create_mock_set_options_tx_envelope()
1106 }
1107
1108 fn dummy_ledger_key() -> LedgerKey {
1109 LedgerKey::Account(LedgerKeyAccount {
1110 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1111 })
1112 }
1113
1114 pub fn mock_account_entry(account_id: &str) -> AccountEntry {
1115 AccountEntry {
1116 account_id: AccountId(PublicKey::from_str(account_id).unwrap()),
1117 balance: 0,
1118 ext: AccountEntryExt::V0,
1119 flags: 0,
1120 home_domain: String32::default(),
1121 inflation_dest: None,
1122 seq_num: 0.into(),
1123 num_sub_entries: 0,
1124 signers: VecM::default(),
1125 thresholds: Thresholds([0, 0, 0, 0]),
1126 }
1127 }
1128
1129 fn dummy_account_entry() -> AccountEntry {
1130 mock_account_entry("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
1131 }
1132
1133 fn create_test_provider_config(configs: Vec<RpcConfig>, timeout: u64) -> ProviderConfig {
1138 ProviderConfig::new(configs, timeout, 3, 60, 60)
1139 }
1140
1141 #[test]
1142 fn test_new_provider() {
1143 let _env_guard = setup_test_env();
1144
1145 let provider = StellarProvider::new(create_test_provider_config(
1146 vec![RpcConfig::new("http://localhost:8000".to_string())],
1147 0,
1148 ));
1149 assert!(provider.is_ok());
1150
1151 let provider_err = StellarProvider::new(create_test_provider_config(vec![], 0));
1152 assert!(provider_err.is_err());
1153 match provider_err.unwrap_err() {
1154 ProviderError::NetworkConfiguration(msg) => {
1155 assert!(msg.contains("No RPC configurations provided"));
1156 }
1157 _ => panic!("Unexpected error type"),
1158 }
1159 }
1160
1161 #[test]
1162 fn test_new_provider_selects_highest_weight() {
1163 let _env_guard = setup_test_env();
1164
1165 let configs = vec![
1166 RpcConfig::with_weight("http://rpc1.example.com".to_string(), 10).unwrap(),
1167 RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), RpcConfig::with_weight("http://rpc3.example.com".to_string(), 50).unwrap(),
1169 ];
1170 let provider = StellarProvider::new(create_test_provider_config(configs, 0));
1171 assert!(provider.is_ok());
1172 }
1176
1177 #[test]
1178 fn test_new_provider_ignores_weight_zero() {
1179 let _env_guard = setup_test_env();
1180
1181 let configs = vec![
1182 RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(), RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), ];
1185 let provider = StellarProvider::new(create_test_provider_config(configs, 0));
1186 assert!(provider.is_ok());
1187
1188 let configs_only_zero =
1189 vec![RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap()];
1190 let provider_err = StellarProvider::new(create_test_provider_config(configs_only_zero, 0));
1191 assert!(provider_err.is_err());
1192 match provider_err.unwrap_err() {
1193 ProviderError::NetworkConfiguration(msg) => {
1194 assert!(msg.contains("No active RPC configurations provided"));
1195 }
1196 _ => panic!("Unexpected error type"),
1197 }
1198 }
1199
1200 #[test]
1201 fn test_new_provider_invalid_url_scheme() {
1202 let configs = vec![RpcConfig::new("ftp://invalid.example.com".to_string())];
1203 let provider_err = StellarProvider::new(create_test_provider_config(configs, 0));
1204 assert!(provider_err.is_err());
1205 match provider_err.unwrap_err() {
1206 ProviderError::NetworkConfiguration(msg) => {
1207 assert!(msg.contains("Invalid URL scheme"));
1208 }
1209 _ => panic!("Unexpected error type"),
1210 }
1211 }
1212
1213 #[test]
1214 fn test_new_provider_all_zero_weight_configs() {
1215 let _env_guard = setup_test_env();
1216
1217 let configs = vec![
1218 RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(),
1219 RpcConfig::with_weight("http://rpc2.example.com".to_string(), 0).unwrap(),
1220 ];
1221 let provider_err = StellarProvider::new(create_test_provider_config(configs, 0));
1222 assert!(provider_err.is_err());
1223 match provider_err.unwrap_err() {
1224 ProviderError::NetworkConfiguration(msg) => {
1225 assert!(msg.contains("No active RPC configurations provided"));
1226 }
1227 _ => panic!("Unexpected error type"),
1228 }
1229 }
1230
1231 #[tokio::test]
1232 async fn test_mock_basic_methods() {
1233 let mut mock = MockStellarProviderTrait::new();
1234
1235 mock.expect_get_network()
1236 .times(1)
1237 .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
1238
1239 mock.expect_get_latest_ledger()
1240 .times(1)
1241 .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
1242
1243 assert!(mock.get_network().await.is_ok());
1244 assert!(mock.get_latest_ledger().await.is_ok());
1245 }
1246
1247 #[tokio::test]
1248 async fn test_mock_transaction_flow() {
1249 let mut mock = MockStellarProviderTrait::new();
1250
1251 let envelope: TransactionEnvelope = dummy_transaction_envelope();
1252 let hash = dummy_hash();
1253
1254 mock.expect_simulate_transaction_envelope()
1255 .withf(|_| true)
1256 .times(1)
1257 .returning(|_| async { Ok(dummy_simulate()) }.boxed());
1258
1259 mock.expect_send_transaction()
1260 .withf(|_| true)
1261 .times(1)
1262 .returning(|_| async { Ok(dummy_hash()) }.boxed());
1263
1264 mock.expect_send_transaction_polling()
1265 .withf(|_| true)
1266 .times(1)
1267 .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
1268
1269 mock.expect_get_transaction()
1270 .withf(|_| true)
1271 .times(1)
1272 .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
1273
1274 mock.simulate_transaction_envelope(&envelope).await.unwrap();
1275 mock.send_transaction(&envelope).await.unwrap();
1276 mock.send_transaction_polling(&envelope).await.unwrap();
1277 mock.get_transaction(&hash).await.unwrap();
1278 }
1279
1280 #[tokio::test]
1281 async fn test_mock_events_and_entries() {
1282 let mut mock = MockStellarProviderTrait::new();
1283
1284 mock.expect_get_events()
1285 .times(1)
1286 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1287
1288 mock.expect_get_ledger_entries()
1289 .times(1)
1290 .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
1291
1292 let events_request = GetEventsRequest {
1293 start: EventStart::Ledger(1),
1294 event_type: None,
1295 contract_ids: vec![],
1296 topics: vec![],
1297 limit: Some(10),
1298 };
1299
1300 let dummy_key: LedgerKey = dummy_ledger_key();
1301 mock.get_events(events_request).await.unwrap();
1302 mock.get_ledger_entries(&[dummy_key]).await.unwrap();
1303 }
1304
1305 #[tokio::test]
1306 async fn test_mock_all_methods_ok() {
1307 let mut mock = MockStellarProviderTrait::new();
1308
1309 mock.expect_get_account()
1310 .with(p::eq("GTESTACCOUNTID"))
1311 .times(1)
1312 .returning(|_| async { Ok(dummy_account_entry()) }.boxed());
1313
1314 mock.expect_simulate_transaction_envelope()
1315 .times(1)
1316 .returning(|_| async { Ok(dummy_simulate()) }.boxed());
1317
1318 mock.expect_send_transaction_polling()
1319 .times(1)
1320 .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
1321
1322 mock.expect_get_network()
1323 .times(1)
1324 .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
1325
1326 mock.expect_get_latest_ledger()
1327 .times(1)
1328 .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
1329
1330 mock.expect_send_transaction()
1331 .times(1)
1332 .returning(|_| async { Ok(dummy_hash()) }.boxed());
1333
1334 mock.expect_get_transaction()
1335 .times(1)
1336 .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
1337
1338 mock.expect_get_transactions()
1339 .times(1)
1340 .returning(|_| async { Ok(dummy_get_transactions_response()) }.boxed());
1341
1342 mock.expect_get_ledger_entries()
1343 .times(1)
1344 .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
1345
1346 mock.expect_get_events()
1347 .times(1)
1348 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1349
1350 let _ = mock.get_account("GTESTACCOUNTID").await.unwrap();
1351 let env: TransactionEnvelope = dummy_transaction_envelope();
1352 mock.simulate_transaction_envelope(&env).await.unwrap();
1353 mock.send_transaction_polling(&env).await.unwrap();
1354 mock.get_network().await.unwrap();
1355 mock.get_latest_ledger().await.unwrap();
1356 mock.send_transaction(&env).await.unwrap();
1357
1358 let h = dummy_hash();
1359 mock.get_transaction(&h).await.unwrap();
1360
1361 let req: GetTransactionsRequest = GetTransactionsRequest {
1362 start_ledger: None,
1363 pagination: None,
1364 };
1365 mock.get_transactions(req).await.unwrap();
1366
1367 let key: LedgerKey = dummy_ledger_key();
1368 mock.get_ledger_entries(&[key]).await.unwrap();
1369
1370 let ev_req = GetEventsRequest {
1371 start: EventStart::Ledger(0),
1372 event_type: None,
1373 contract_ids: vec![],
1374 topics: vec![],
1375 limit: None,
1376 };
1377 mock.get_events(ev_req).await.unwrap();
1378 }
1379
1380 #[tokio::test]
1381 async fn test_error_propagation() {
1382 let mut mock = MockStellarProviderTrait::new();
1383
1384 mock.expect_get_account()
1385 .returning(|_| async { Err(ProviderError::Other("boom".to_string())) }.boxed());
1386
1387 let res = mock.get_account("BAD").await;
1388 assert!(res.is_err());
1389 assert!(res.unwrap_err().to_string().contains("boom"));
1390 }
1391
1392 #[tokio::test]
1393 async fn test_get_events_edge_cases() {
1394 let mut mock = MockStellarProviderTrait::new();
1395
1396 mock.expect_get_events()
1397 .withf(|req| {
1398 req.contract_ids.is_empty() && req.topics.is_empty() && req.limit.is_none()
1399 })
1400 .times(1)
1401 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1402
1403 let ev_req = GetEventsRequest {
1404 start: EventStart::Ledger(0),
1405 event_type: None,
1406 contract_ids: vec![],
1407 topics: vec![],
1408 limit: None,
1409 };
1410
1411 mock.get_events(ev_req).await.unwrap();
1412 }
1413
1414 #[test]
1415 fn test_provider_send_sync_bounds() {
1416 fn assert_send_sync<T: Send + Sync>() {}
1417 assert_send_sync::<StellarProvider>();
1418 }
1419
1420 #[cfg(test)]
1421 mod concrete_tests {
1422 use super::*;
1423
1424 const NON_EXISTENT_URL: &str = "http://127.0.0.1:9998";
1425
1426 fn create_test_provider_config(configs: Vec<RpcConfig>, timeout: u64) -> ProviderConfig {
1427 ProviderConfig::new(configs, timeout, 3, 60, 60)
1428 }
1429
1430 fn setup_provider() -> StellarProvider {
1431 StellarProvider::new(create_test_provider_config(
1432 vec![RpcConfig::new(NON_EXISTENT_URL.to_string())],
1433 0,
1434 ))
1435 .expect("Provider creation should succeed even with bad URL")
1436 }
1437
1438 #[tokio::test]
1439 async fn test_concrete_get_account_error() {
1440 let _env_guard = setup_test_env();
1441 let provider = setup_provider();
1442 let result = provider.get_account("SOME_ACCOUNT_ID").await;
1443 assert!(result.is_err());
1444 let err_str = result.unwrap_err().to_string();
1445 assert!(
1447 err_str.contains("Failed to get account"),
1448 "Unexpected error message: {err_str}"
1449 );
1450 }
1451
1452 #[tokio::test]
1453 async fn test_concrete_simulate_transaction_envelope_error() {
1454 let _env_guard = setup_test_env();
1455
1456 let provider = setup_provider();
1457 let envelope: TransactionEnvelope = dummy_transaction_envelope();
1458 let result = provider.simulate_transaction_envelope(&envelope).await;
1459 assert!(result.is_err());
1460 let err_str = result.unwrap_err().to_string();
1461 assert!(
1463 err_str.contains("Failed to simulate transaction"),
1464 "Unexpected error message: {err_str}"
1465 );
1466 }
1467
1468 #[tokio::test]
1469 async fn test_concrete_send_transaction_polling_error() {
1470 let _env_guard = setup_test_env();
1471
1472 let provider = setup_provider();
1473 let envelope: TransactionEnvelope = dummy_transaction_envelope();
1474 let result = provider.send_transaction_polling(&envelope).await;
1475 assert!(result.is_err());
1476 let err_str = result.unwrap_err().to_string();
1477 assert!(
1479 err_str.contains("Failed to send transaction (polling)"),
1480 "Unexpected error message: {err_str}"
1481 );
1482 }
1483
1484 #[tokio::test]
1485 async fn test_concrete_get_network_error() {
1486 let _env_guard = setup_test_env();
1487
1488 let provider = setup_provider();
1489 let result = provider.get_network().await;
1490 assert!(result.is_err());
1491 let err_str = result.unwrap_err().to_string();
1492 assert!(
1494 err_str.contains("Failed to get network"),
1495 "Unexpected error message: {err_str}"
1496 );
1497 }
1498
1499 #[tokio::test]
1500 async fn test_concrete_get_latest_ledger_error() {
1501 let _env_guard = setup_test_env();
1502
1503 let provider = setup_provider();
1504 let result = provider.get_latest_ledger().await;
1505 assert!(result.is_err());
1506 let err_str = result.unwrap_err().to_string();
1507 assert!(
1509 err_str.contains("Failed to get latest ledger"),
1510 "Unexpected error message: {err_str}"
1511 );
1512 }
1513
1514 #[tokio::test]
1515 async fn test_concrete_send_transaction_error() {
1516 let _env_guard = setup_test_env();
1517
1518 let provider = setup_provider();
1519 let envelope: TransactionEnvelope = dummy_transaction_envelope();
1520 let result = provider.send_transaction(&envelope).await;
1521 assert!(result.is_err());
1522 let err_str = result.unwrap_err().to_string();
1523 assert!(
1525 err_str.contains("Failed to send transaction"),
1526 "Unexpected error message: {err_str}"
1527 );
1528 }
1529
1530 #[tokio::test]
1531 async fn test_concrete_get_transaction_error() {
1532 let _env_guard = setup_test_env();
1533
1534 let provider = setup_provider();
1535 let hash: Hash = dummy_hash();
1536 let result = provider.get_transaction(&hash).await;
1537 assert!(result.is_err());
1538 let err = result.unwrap_err();
1541 assert!(
1542 matches!(
1543 err,
1544 ProviderError::Other(_)
1545 | ProviderError::Timeout
1546 | ProviderError::NetworkConfiguration(_)
1547 ),
1548 "Unexpected error: {err}"
1549 );
1550 }
1551
1552 #[tokio::test]
1553 async fn test_concrete_get_transactions_error() {
1554 let _env_guard = setup_test_env();
1555
1556 let provider = setup_provider();
1557 let req = GetTransactionsRequest {
1558 start_ledger: None,
1559 pagination: None,
1560 };
1561 let result = provider.get_transactions(req).await;
1562 assert!(result.is_err());
1563 let err_str = result.unwrap_err().to_string();
1564 assert!(
1566 err_str.contains("Failed to get transactions"),
1567 "Unexpected error message: {err_str}"
1568 );
1569 }
1570
1571 #[tokio::test]
1572 async fn test_concrete_get_ledger_entries_error() {
1573 let _env_guard = setup_test_env();
1574
1575 let provider = setup_provider();
1576 let key: LedgerKey = dummy_ledger_key();
1577 let result = provider.get_ledger_entries(&[key]).await;
1578 assert!(result.is_err());
1579 let err_str = result.unwrap_err().to_string();
1580 assert!(
1582 err_str.contains("Failed to get ledger entries"),
1583 "Unexpected error message: {err_str}"
1584 );
1585 }
1586
1587 #[tokio::test]
1588 async fn test_concrete_get_events_error() {
1589 let _env_guard = setup_test_env();
1590 let provider = setup_provider();
1591 let req = GetEventsRequest {
1592 start: EventStart::Ledger(1),
1593 event_type: None,
1594 contract_ids: vec![],
1595 topics: vec![],
1596 limit: None,
1597 };
1598 let result = provider.get_events(req).await;
1599 assert!(result.is_err());
1600 let err_str = result.unwrap_err().to_string();
1601 assert!(
1603 err_str.contains("Failed to get events"),
1604 "Unexpected error message: {err_str}"
1605 );
1606 }
1607 }
1608
1609 #[test]
1610 fn test_generate_unique_rpc_id() {
1611 let id1 = generate_unique_rpc_id();
1612 let id2 = generate_unique_rpc_id();
1613 assert_ne!(id1, id2, "Generated IDs should be unique");
1614 assert!(id1 > 0, "ID should be positive");
1615 assert!(id2 > 0, "ID should be positive");
1616 assert!(id2 > id1, "IDs should be monotonically increasing");
1617 }
1618
1619 #[test]
1620 fn test_normalize_url_for_log() {
1621 assert_eq!(
1623 normalize_url_for_log("https://api.example.com/path"),
1624 "https://api.example.com/path"
1625 );
1626
1627 assert_eq!(
1629 normalize_url_for_log("https://api.example.com/path?api_key=secret&other=value"),
1630 "https://api.example.com/path"
1631 );
1632
1633 assert_eq!(
1635 normalize_url_for_log("https://api.example.com/path#section"),
1636 "https://api.example.com/path"
1637 );
1638
1639 assert_eq!(
1641 normalize_url_for_log("https://api.example.com/path?key=value#fragment"),
1642 "https://api.example.com/path"
1643 );
1644
1645 assert_eq!(
1647 normalize_url_for_log("https://user:password@api.example.com/path"),
1648 "https://<redacted>@api.example.com/path"
1649 );
1650
1651 assert_eq!(
1653 normalize_url_for_log("https://user:pass@api.example.com/path?token=abc#frag"),
1654 "https://<redacted>@api.example.com/path"
1655 );
1656
1657 assert_eq!(
1659 normalize_url_for_log("https://api.example.com/path?token=abc"),
1660 "https://api.example.com/path"
1661 );
1662
1663 assert_eq!(normalize_url_for_log("not-a-url"), "not-a-url");
1665 }
1666
1667 #[test]
1668 fn test_categorize_stellar_error_with_context_timeout() {
1669 let err = StellarClientError::TransactionSubmissionTimeout;
1670 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1671 assert!(matches!(result, ProviderError::Timeout));
1672 }
1673
1674 #[test]
1675 fn test_categorize_stellar_error_with_context_xdr_error() {
1676 use soroban_rs::xdr::Error as XdrError;
1677 let err = StellarClientError::Xdr(XdrError::Invalid);
1678 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1679 match result {
1680 ProviderError::Other(msg) => {
1681 assert!(msg.contains("Test operation"));
1682 }
1683 _ => panic!("Expected Other error"),
1684 }
1685 }
1686
1687 #[test]
1688 fn test_categorize_stellar_error_with_context_serde_error() {
1689 let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
1691 let err = StellarClientError::Serde(json_err);
1692 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1693 match result {
1694 ProviderError::Other(msg) => {
1695 assert!(msg.contains("Test operation"));
1696 }
1697 _ => panic!("Expected Other error"),
1698 }
1699 }
1700
1701 #[test]
1702 fn test_categorize_stellar_error_with_context_url_errors() {
1703 let invalid_uri_err: http::uri::InvalidUri =
1705 ":::invalid url".parse::<http::Uri>().unwrap_err();
1706 let err = StellarClientError::InvalidRpcUrl(invalid_uri_err);
1707 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1708 match result {
1709 ProviderError::NetworkConfiguration(msg) => {
1710 assert!(msg.contains("Test operation"));
1711 assert!(msg.contains("Invalid RPC URL"));
1712 }
1713 _ => panic!("Expected NetworkConfiguration error"),
1714 }
1715
1716 let err = StellarClientError::InvalidUrl("not a url".to_string());
1718 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1719 match result {
1720 ProviderError::NetworkConfiguration(msg) => {
1721 assert!(msg.contains("Test operation"));
1722 assert!(msg.contains("Invalid URL"));
1723 }
1724 _ => panic!("Expected NetworkConfiguration error"),
1725 }
1726 }
1727
1728 #[test]
1729 fn test_categorize_stellar_error_with_context_network_passphrase() {
1730 let err = StellarClientError::InvalidNetworkPassphrase {
1731 expected: "Expected".to_string(),
1732 server: "Server".to_string(),
1733 };
1734 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1735 match result {
1736 ProviderError::NetworkConfiguration(msg) => {
1737 assert!(msg.contains("Test operation"));
1738 assert!(msg.contains("Expected"));
1739 assert!(msg.contains("Server"));
1740 }
1741 _ => panic!("Expected NetworkConfiguration error"),
1742 }
1743 }
1744
1745 #[test]
1746 fn test_categorize_stellar_error_with_context_json_rpc_call_error() {
1747 let err = StellarClientError::TransactionSubmissionTimeout;
1751 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1752 assert!(matches!(result, ProviderError::Timeout));
1754 }
1755
1756 #[test]
1757 fn test_categorize_stellar_error_with_context_json_rpc_timeout() {
1758 let err = StellarClientError::TransactionSubmissionTimeout;
1760 let result = categorize_stellar_error_with_context(err, None);
1761 assert!(matches!(result, ProviderError::Timeout));
1762 }
1763
1764 #[test]
1765 fn test_categorize_stellar_error_with_context_transport_errors() {
1766 let err = StellarClientError::InvalidResponse;
1768 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1769 match result {
1770 ProviderError::Other(msg) => {
1771 assert!(msg.contains("Test operation"));
1772 assert!(msg.contains("Invalid response"));
1773 }
1774 _ => panic!("Expected Other error for response issues"),
1775 }
1776 }
1777
1778 #[test]
1779 fn test_categorize_stellar_error_with_context_response_errors() {
1780 let err = StellarClientError::InvalidResponse;
1782 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1783 match result {
1784 ProviderError::Other(msg) => {
1785 assert!(msg.contains("Test operation"));
1786 assert!(msg.contains("Invalid response"));
1787 }
1788 _ => panic!("Expected Other error"),
1789 }
1790
1791 let err = StellarClientError::MissingResult;
1793 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1794 match result {
1795 ProviderError::Other(msg) => {
1796 assert!(msg.contains("Test operation"));
1797 assert!(msg.contains("Missing result"));
1798 }
1799 _ => panic!("Expected Other error"),
1800 }
1801 }
1802
1803 #[test]
1804 fn test_categorize_stellar_error_with_context_transaction_errors() {
1805 let err = StellarClientError::TransactionFailed("tx failed".to_string());
1807 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1808 match result {
1809 ProviderError::Other(msg) => {
1810 assert!(msg.contains("Test operation"));
1811 assert!(msg.contains("tx failed"));
1812 }
1813 _ => panic!("Expected Other error"),
1814 }
1815
1816 let err = StellarClientError::NotFound("Account".to_string(), "123".to_string());
1818 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1819 match result {
1820 ProviderError::Other(msg) => {
1821 assert!(msg.contains("Test operation"));
1822 assert!(msg.contains("Account not found"));
1823 assert!(msg.contains("123"));
1824 }
1825 _ => panic!("Expected Other error"),
1826 }
1827 }
1828
1829 #[test]
1830 fn test_categorize_stellar_error_with_context_validation_errors() {
1831 let err = StellarClientError::InvalidCursor;
1833 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1834 match result {
1835 ProviderError::Other(msg) => {
1836 assert!(msg.contains("Test operation"));
1837 assert!(msg.contains("Invalid cursor"));
1838 }
1839 _ => panic!("Expected Other error"),
1840 }
1841
1842 let err = StellarClientError::LargeFee(1000000);
1844 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1845 match result {
1846 ProviderError::Other(msg) => {
1847 assert!(msg.contains("Test operation"));
1848 assert!(msg.contains("1000000"));
1849 }
1850 _ => panic!("Expected Other error"),
1851 }
1852 }
1853
1854 #[test]
1855 fn test_categorize_stellar_error_with_context_no_context() {
1856 let err = StellarClientError::InvalidResponse;
1858 let result = categorize_stellar_error_with_context(err, None);
1859 match result {
1860 ProviderError::Other(msg) => {
1861 assert!(!msg.contains(":")); assert!(msg.contains("Invalid response"));
1863 }
1864 _ => panic!("Expected Other error"),
1865 }
1866 }
1867
1868 #[test]
1869 fn test_initialize_provider_invalid_url() {
1870 let _env_guard = setup_test_env();
1871 let provider = StellarProvider::new(create_test_provider_config(
1872 vec![RpcConfig::new("http://localhost:8000".to_string())],
1873 30,
1874 ))
1875 .unwrap();
1876
1877 let result = provider.initialize_provider("invalid-url");
1879 assert!(result.is_err());
1880 match result.unwrap_err() {
1881 ProviderError::NetworkConfiguration(msg) => {
1882 assert!(
1884 msg.contains("Failed to create Stellar RPC client")
1885 || msg.contains("RPC URL security validation failed")
1886 );
1887 }
1888 _ => panic!("Expected NetworkConfiguration error"),
1889 }
1890 }
1891
1892 #[test]
1893 fn test_initialize_raw_provider_timeout_config() {
1894 let _env_guard = setup_test_env();
1895 let provider = StellarProvider::new(create_test_provider_config(
1896 vec![RpcConfig::new("http://localhost:8000".to_string())],
1897 30,
1898 ))
1899 .unwrap();
1900
1901 let result = provider.initialize_raw_provider("http://localhost:8000");
1903 assert!(result.is_ok());
1904
1905 let result = provider.initialize_raw_provider("not-a-url");
1908 assert!(result.is_ok() || result.is_err());
1911 }
1912
1913 #[tokio::test]
1914 async fn test_raw_request_dyn_success() {
1915 let _env_guard = setup_test_env();
1916
1917 let provider = StellarProvider::new(create_test_provider_config(
1919 vec![RpcConfig::new("http://127.0.0.1:9999".to_string())],
1920 1,
1921 ))
1922 .unwrap();
1923
1924 let params = serde_json::json!({"test": "value"});
1925 let result = provider
1926 .raw_request_dyn("test_method", params, Some(JsonRpcId::Number(1)))
1927 .await;
1928
1929 assert!(result.is_err());
1931 let err = result.unwrap_err();
1932 assert!(matches!(
1934 err,
1935 ProviderError::Other(_)
1936 | ProviderError::Timeout
1937 | ProviderError::NetworkConfiguration(_)
1938 ));
1939 }
1940
1941 #[tokio::test]
1942 async fn test_raw_request_dyn_with_auto_generated_id() {
1943 let _env_guard = setup_test_env();
1944
1945 let provider = StellarProvider::new(create_test_provider_config(
1946 vec![RpcConfig::new("http://127.0.0.1:9999".to_string())],
1947 1,
1948 ))
1949 .unwrap();
1950
1951 let params = serde_json::json!({"test": "value"});
1952 let result = provider.raw_request_dyn("test_method", params, None).await;
1953
1954 assert!(result.is_err());
1956 }
1957
1958 #[tokio::test]
1959 async fn test_retry_raw_request_connection_failure() {
1960 let _env_guard = setup_test_env();
1961
1962 let provider = StellarProvider::new(create_test_provider_config(
1963 vec![RpcConfig::new("http://127.0.0.1:9999".to_string())],
1964 1,
1965 ))
1966 .unwrap();
1967
1968 let request = serde_json::json!({
1969 "jsonrpc": "2.0",
1970 "id": 1,
1971 "method": "test",
1972 "params": {}
1973 });
1974
1975 let result = provider.retry_raw_request("test_operation", request).await;
1976
1977 assert!(result.is_err());
1979 let err = result.unwrap_err();
1980 assert!(matches!(
1982 err,
1983 ProviderError::Other(_) | ProviderError::Timeout
1984 ));
1985 }
1986
1987 #[tokio::test]
1988 async fn test_raw_request_dyn_json_rpc_error_response() {
1989 let _env_guard = setup_test_env();
1990
1991 let provider = StellarProvider::new(create_test_provider_config(
1994 vec![RpcConfig::new("http://127.0.0.1:9999".to_string())],
1995 1,
1996 ))
1997 .unwrap();
1998
1999 let params = serde_json::json!({"test": "value"});
2000 let result = provider
2001 .raw_request_dyn(
2002 "test_method",
2003 params,
2004 Some(JsonRpcId::String("test-id".to_string())),
2005 )
2006 .await;
2007
2008 assert!(result.is_err());
2010 }
2011
2012 #[tokio::test]
2013 async fn test_get_transaction_zstd_compressed_response() {
2014 let _env_guard = setup_test_env();
2015
2016 let mut mock_server = mockito::Server::new_async().await;
2017
2018 let response_body = serde_json::json!({
2019 "jsonrpc": "2.0",
2020 "id": 1,
2021 "result": {
2022 "status": "NOT_FOUND",
2023 }
2024 });
2025 let compressed = zstd::encode_all(response_body.to_string().as_bytes(), 3).unwrap();
2026
2027 let mock = mock_server
2028 .mock("POST", "/")
2029 .match_header(
2031 "accept-encoding",
2032 mockito::Matcher::Regex("zstd".to_string()),
2033 )
2034 .match_body(mockito::Matcher::PartialJson(serde_json::json!({
2036 "method": "getTransaction",
2037 "params": { "hash": "0".repeat(64) },
2038 })))
2039 .with_header("content-encoding", "zstd")
2040 .with_body(compressed)
2041 .create_async()
2042 .await;
2043
2044 let provider = StellarProvider::new(create_test_provider_config(
2045 vec![RpcConfig::new(mock_server.url())],
2046 5,
2047 ))
2048 .unwrap();
2049
2050 let response = provider.get_transaction(&dummy_hash()).await.unwrap();
2051 assert_eq!(response.status, "NOT_FOUND");
2052 mock.assert_async().await;
2053 }
2054
2055 #[tokio::test]
2056 async fn test_raw_path_http_429_is_classified_and_retried() {
2057 let _env_guard = setup_test_env();
2058 std::env::set_var("PROVIDER_MAX_RETRIES", "2");
2060
2061 let mut mock_server = mockito::Server::new_async().await;
2062 let mock = mock_server
2063 .mock("POST", "/")
2064 .with_status(429)
2065 .with_body("rate limited, non-JSON body")
2066 .expect(2)
2067 .create_async()
2068 .await;
2069
2070 let provider = StellarProvider::new(create_test_provider_config(
2071 vec![RpcConfig::new(mock_server.url())],
2072 5,
2073 ))
2074 .unwrap();
2075
2076 let err = provider.get_transaction(&dummy_hash()).await.unwrap_err();
2077 assert!(
2078 matches!(err, ProviderError::RateLimited),
2079 "Unexpected error: {err}"
2080 );
2081 mock.assert_async().await;
2082 }
2083
2084 #[tokio::test]
2085 async fn test_raw_path_retriable_json_rpc_error_is_retried() {
2086 let _env_guard = setup_test_env();
2087 std::env::set_var("PROVIDER_MAX_RETRIES", "2");
2088
2089 let mut mock_server = mockito::Server::new_async().await;
2090 let mock = mock_server
2091 .mock("POST", "/")
2092 .with_body(
2093 serde_json::json!({
2094 "jsonrpc": "2.0",
2095 "id": 1,
2096 "error": { "code": -32005, "message": "limit exceeded" }
2097 })
2098 .to_string(),
2099 )
2100 .expect(2)
2101 .create_async()
2102 .await;
2103
2104 let provider = StellarProvider::new(create_test_provider_config(
2105 vec![RpcConfig::new(mock_server.url())],
2106 5,
2107 ))
2108 .unwrap();
2109
2110 let err = provider.get_transaction(&dummy_hash()).await.unwrap_err();
2111 assert!(
2112 matches!(err, ProviderError::RpcErrorCode { code: -32005, .. }),
2113 "Unexpected error: {err}"
2114 );
2115 mock.assert_async().await;
2116 }
2117
2118 #[tokio::test]
2119 async fn test_raw_path_non_retriable_json_rpc_error_fails_fast() {
2120 let _env_guard = setup_test_env();
2121 std::env::set_var("PROVIDER_MAX_RETRIES", "2");
2122
2123 let mut mock_server = mockito::Server::new_async().await;
2124 let mock = mock_server
2125 .mock("POST", "/")
2126 .with_body(
2127 serde_json::json!({
2128 "jsonrpc": "2.0",
2129 "id": 1,
2130 "error": { "code": -32601, "message": "method not found" }
2131 })
2132 .to_string(),
2133 )
2134 .expect(1)
2135 .create_async()
2136 .await;
2137
2138 let provider = StellarProvider::new(create_test_provider_config(
2139 vec![RpcConfig::new(mock_server.url())],
2140 5,
2141 ))
2142 .unwrap();
2143
2144 let err = provider.get_transaction(&dummy_hash()).await.unwrap_err();
2145 assert!(
2146 matches!(err, ProviderError::RpcErrorCode { code: -32601, .. }),
2147 "Unexpected error: {err}"
2148 );
2149 mock.assert_async().await;
2150 }
2151
2152 #[tokio::test]
2153 async fn test_raw_path_null_error_field_is_treated_as_success() {
2154 let _env_guard = setup_test_env();
2155
2156 let mut mock_server = mockito::Server::new_async().await;
2157 let mock = mock_server
2159 .mock("POST", "/")
2160 .with_body(
2161 serde_json::json!({
2162 "jsonrpc": "2.0",
2163 "id": 1,
2164 "error": null,
2165 "result": { "status": "NOT_FOUND" }
2166 })
2167 .to_string(),
2168 )
2169 .expect(1)
2170 .create_async()
2171 .await;
2172
2173 let provider = StellarProvider::new(create_test_provider_config(
2174 vec![RpcConfig::new(mock_server.url())],
2175 5,
2176 ))
2177 .unwrap();
2178
2179 let response = provider.get_transaction(&dummy_hash()).await.unwrap();
2180 assert_eq!(response.status, "NOT_FOUND");
2181 mock.assert_async().await;
2182 }
2183
2184 #[tokio::test]
2185 async fn test_raw_path_http_error_surfaces_provider_body() {
2186 let _env_guard = setup_test_env();
2187 std::env::set_var("PROVIDER_MAX_RETRIES", "1");
2189
2190 let mut mock_server = mockito::Server::new_async().await;
2191 let mock = mock_server
2192 .mock("POST", "/")
2193 .with_status(400)
2194 .with_body("provider says: bad params")
2195 .expect(1)
2196 .create_async()
2197 .await;
2198
2199 let provider = StellarProvider::new(create_test_provider_config(
2200 vec![RpcConfig::new(mock_server.url())],
2201 5,
2202 ))
2203 .unwrap();
2204
2205 let err = provider.get_transaction(&dummy_hash()).await.unwrap_err();
2206 match err {
2207 ProviderError::RequestError { error, status_code } => {
2208 assert_eq!(status_code, 400);
2209 assert!(
2210 error.contains("provider says: bad params"),
2211 "Body detail not surfaced: {error}"
2212 );
2213 }
2214 other => panic!("Unexpected error: {other}"),
2215 }
2216 mock.assert_async().await;
2217 }
2218
2219 #[test]
2220 fn test_provider_creation_edge_cases() {
2221 let _env_guard = setup_test_env();
2222
2223 let result = StellarProvider::new(create_test_provider_config(vec![], 30));
2225 assert!(result.is_err());
2226 match result.unwrap_err() {
2227 ProviderError::NetworkConfiguration(msg) => {
2228 assert!(msg.contains("No RPC configurations provided"));
2229 }
2230 _ => panic!("Expected NetworkConfiguration error"),
2231 }
2232
2233 let mut config1 = RpcConfig::new("http://localhost:8000".to_string());
2235 config1.weight = 0;
2236 let mut config2 = RpcConfig::new("http://localhost:8001".to_string());
2237 config2.weight = 0;
2238 let configs = vec![config1, config2];
2239 let result = StellarProvider::new(create_test_provider_config(configs, 30));
2240 assert!(result.is_err());
2241 match result.unwrap_err() {
2242 ProviderError::NetworkConfiguration(msg) => {
2243 assert!(msg.contains("No active RPC configurations"));
2244 }
2245 _ => panic!("Expected NetworkConfiguration error"),
2246 }
2247 }
2248
2249 #[tokio::test]
2250 async fn test_get_events_empty_request() {
2251 let _env_guard = setup_test_env();
2252
2253 let mut mock = MockStellarProviderTrait::new();
2254 mock.expect_get_events()
2255 .withf(|req| req.contract_ids.is_empty() && req.topics.is_empty())
2256 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
2257
2258 let req = GetEventsRequest {
2259 start: EventStart::Ledger(1),
2260 event_type: Some(EventType::Contract),
2261 contract_ids: vec![],
2262 topics: vec![],
2263 limit: Some(10),
2264 };
2265
2266 let result = mock.get_events(req).await;
2267 assert!(result.is_ok());
2268 }
2269
2270 #[tokio::test]
2271 async fn test_get_ledger_entries_empty_keys() {
2272 let _env_guard = setup_test_env();
2273
2274 let mut mock = MockStellarProviderTrait::new();
2275 mock.expect_get_ledger_entries()
2276 .withf(|keys| keys.is_empty())
2277 .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
2278
2279 let result = mock.get_ledger_entries(&[]).await;
2280 assert!(result.is_ok());
2281 }
2282
2283 #[tokio::test]
2284 async fn test_send_transaction_polling_success() {
2285 let _env_guard = setup_test_env();
2286
2287 let mut mock = MockStellarProviderTrait::new();
2288 mock.expect_send_transaction_polling()
2289 .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
2290
2291 let envelope = dummy_transaction_envelope();
2292 let result = mock.send_transaction_polling(&envelope).await;
2293 assert!(result.is_ok());
2294 }
2295
2296 #[tokio::test]
2297 async fn test_get_transactions_with_pagination() {
2298 let _env_guard = setup_test_env();
2299
2300 let mut mock = MockStellarProviderTrait::new();
2301 mock.expect_get_transactions()
2302 .returning(|_| async { Ok(dummy_get_transactions_response()) }.boxed());
2303
2304 let req = GetTransactionsRequest {
2305 start_ledger: Some(1000),
2306 pagination: None, };
2308
2309 let result = mock.get_transactions(req).await;
2310 assert!(result.is_ok());
2311 }
2312}