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, GetTransactionsRequest,
13 GetTransactionsResponse, SimulateTransactionResponse,
14};
15use soroban_rs::xdr::{AccountEntry, Hash, LedgerKey, TransactionEnvelope};
16#[cfg(test)]
17use soroban_rs::xdr::{AccountId, LedgerKeyAccount, PublicKey, Uint256};
18use soroban_rs::SorobanTransactionResponse;
19use std::sync::atomic::{AtomicU64, Ordering};
20
21#[cfg(test)]
22use mockall::automock;
23
24use crate::models::{JsonRpcId, RpcConfig};
25use crate::services::provider::is_retriable_error;
26use crate::services::provider::retry::retry_rpc_call;
27use crate::services::provider::rpc_selector::RpcSelector;
28use crate::services::provider::should_mark_provider_failed;
29use crate::services::provider::ProviderError;
30use crate::services::provider::RetryConfig;
31use reqwest::Client as ReqwestClient;
34use std::sync::Arc;
35use std::time::Duration;
36
37fn generate_unique_rpc_id() -> u64 {
46 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
47 NEXT_ID.fetch_add(1, Ordering::Relaxed)
48}
49
50fn categorize_stellar_error_with_context(
69 err: StellarClientError,
70 context: Option<&str>,
71) -> ProviderError {
72 let add_context = |msg: String| -> String {
73 match context {
74 Some(ctx) => format!("{ctx}: {msg}"),
75 None => msg,
76 }
77 };
78 match err {
79 StellarClientError::TransactionSubmissionTimeout => ProviderError::Timeout,
81
82 StellarClientError::InvalidAddress(decode_err) => ProviderError::InvalidAddress(
84 add_context(format!("Invalid Stellar address: {decode_err}")),
85 ),
86
87 StellarClientError::Xdr(xdr_err) => {
89 ProviderError::Other(add_context(format!("XDR processing error: {xdr_err}")))
90 }
91
92 StellarClientError::Serde(serde_err) => {
94 ProviderError::Other(add_context(format!("JSON parsing error: {serde_err}")))
95 }
96
97 StellarClientError::InvalidRpcUrl(uri_err) => {
99 ProviderError::NetworkConfiguration(add_context(format!("Invalid RPC URL: {uri_err}")))
100 }
101 StellarClientError::InvalidRpcUrlFromUriParts(uri_err) => {
102 ProviderError::NetworkConfiguration(add_context(format!(
103 "Invalid RPC URL parts: {uri_err}"
104 )))
105 }
106 StellarClientError::InvalidUrl(url) => {
107 ProviderError::NetworkConfiguration(add_context(format!("Invalid URL: {url}")))
108 }
109
110 StellarClientError::InvalidNetworkPassphrase { expected, server } => {
112 ProviderError::NetworkConfiguration(add_context(format!(
113 "Network passphrase mismatch: expected {expected:?}, server returned {server:?}"
114 )))
115 }
116
117 StellarClientError::JsonRpc(jsonrpsee_err) => {
119 match jsonrpsee_err {
120 jsonrpsee_core::error::Error::Call(err_obj) => {
122 let code = err_obj.code() as i64;
123 let message = add_context(err_obj.message().to_string());
124 ProviderError::RpcErrorCode { code, message }
125 }
126
127 jsonrpsee_core::error::Error::RequestTimeout => ProviderError::Timeout,
129
130 jsonrpsee_core::error::Error::Transport(transport_err) => {
132 let mut source = transport_err.source();
134 while let Some(s) = source {
135 if let Some(reqwest_err) = s.downcast_ref::<reqwest::Error>() {
136 return ProviderError::from(reqwest_err);
137 }
138 source = s.source();
139 }
140
141 ProviderError::TransportError(add_context(format!(
142 "Transport error: {transport_err}"
143 )))
144 }
145 other => ProviderError::Other(add_context(format!("JSON-RPC error: {other}"))),
147 }
148 }
149 StellarClientError::InvalidResponse => {
151 ProviderError::Other(add_context(
153 "Invalid response from Stellar RPC server".to_string(),
154 ))
155 }
156 StellarClientError::MissingResult => {
157 ProviderError::Other(add_context("Missing result in RPC response".to_string()))
158 }
159 StellarClientError::MissingError => ProviderError::Other(add_context(
160 "Failed to read error from RPC response".to_string(),
161 )),
162
163 StellarClientError::TransactionFailed(msg) => {
165 ProviderError::Other(add_context(format!("Transaction failed: {msg}")))
166 }
167 StellarClientError::TransactionSubmissionFailed(msg) => {
168 ProviderError::Other(add_context(format!("Transaction submission failed: {msg}")))
169 }
170 StellarClientError::TransactionSimulationFailed(msg) => {
171 ProviderError::Other(add_context(format!("Transaction simulation failed: {msg}")))
172 }
173 StellarClientError::UnexpectedTransactionStatus(status) => ProviderError::Other(
174 add_context(format!("Unexpected transaction status: {status}")),
175 ),
176
177 StellarClientError::NotFound(resource, id) => {
179 ProviderError::Other(add_context(format!("{resource} not found: {id}")))
180 }
181
182 StellarClientError::InvalidCursor => {
184 ProviderError::Other(add_context("Invalid cursor".to_string()))
185 }
186 StellarClientError::UnexpectedSimulateTransactionResultSize { length } => {
187 ProviderError::Other(add_context(format!(
188 "Unexpected simulate transaction result size: {length}"
189 )))
190 }
191 StellarClientError::UnexpectedOperationCount { count } => {
192 ProviderError::Other(add_context(format!("Unexpected operation count: {count}")))
193 }
194 StellarClientError::UnsupportedOperationType => {
195 ProviderError::Other(add_context("Unsupported operation type".to_string()))
196 }
197 StellarClientError::UnexpectedContractCodeDataType(data) => ProviderError::Other(
198 add_context(format!("Unexpected contract code data type: {data:?}")),
199 ),
200 StellarClientError::UnexpectedContractInstance(val) => ProviderError::Other(add_context(
201 format!("Unexpected contract instance: {val:?}"),
202 )),
203 StellarClientError::LargeFee(fee) => {
204 ProviderError::Other(add_context(format!("Fee too large: {fee}")))
205 }
206 StellarClientError::CannotAuthorizeRawTransaction => {
207 ProviderError::Other(add_context("Cannot authorize raw transaction".to_string()))
208 }
209 StellarClientError::MissingOp => {
210 ProviderError::Other(add_context("Missing operation in transaction".to_string()))
211 }
212 StellarClientError::MissingSignerForAddress { address } => ProviderError::Other(
213 add_context(format!("Missing signer for address: {address}")),
214 ),
215
216 #[allow(deprecated)]
218 StellarClientError::UnexpectedToken(entry) => {
219 ProviderError::Other(add_context(format!("Unexpected token: {entry:?}")))
220 }
221 }
222}
223
224fn normalize_url_for_log(url: &str) -> String {
230 let mut s = url.to_string();
232 if let Some(q) = s.find('?') {
233 s.truncate(q);
234 }
235 if let Some(h) = s.find('#') {
236 s.truncate(h);
237 }
238
239 if let Some(scheme_pos) = s.find("://") {
241 let start = scheme_pos + 3;
242 if let Some(at_pos) = s[start..].find('@') {
243 let after = &s[start + at_pos + 1..];
244 let prefix = &s[..start];
245 s = format!("{prefix}<redacted>@{after}");
246 }
247 }
248
249 s
250}
251#[derive(Debug, Clone)]
252pub struct GetEventsRequest {
253 pub start: EventStart,
254 pub event_type: Option<EventType>,
255 pub contract_ids: Vec<String>,
256 pub topics: Vec<String>,
257 pub limit: Option<usize>,
258}
259
260#[derive(Clone, Debug)]
261pub struct StellarProvider {
262 selector: RpcSelector,
264 timeout_seconds: Duration,
266 retry_config: RetryConfig,
268}
269
270#[async_trait]
271#[cfg_attr(test, automock)]
272#[allow(dead_code)]
273pub trait StellarProviderTrait: Send + Sync {
274 async fn get_account(&self, account_id: &str) -> Result<AccountEntry, ProviderError>;
275 async fn simulate_transaction_envelope(
276 &self,
277 tx_envelope: &TransactionEnvelope,
278 ) -> Result<SimulateTransactionResponse, ProviderError>;
279 async fn send_transaction_polling(
280 &self,
281 tx_envelope: &TransactionEnvelope,
282 ) -> Result<SorobanTransactionResponse, ProviderError>;
283 async fn get_network(&self) -> Result<GetNetworkResponse, ProviderError>;
284 async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, ProviderError>;
285 async fn send_transaction(
286 &self,
287 tx_envelope: &TransactionEnvelope,
288 ) -> Result<Hash, ProviderError>;
289 async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, ProviderError>;
290 async fn get_transactions(
291 &self,
292 request: GetTransactionsRequest,
293 ) -> Result<GetTransactionsResponse, ProviderError>;
294 async fn get_ledger_entries(
295 &self,
296 keys: &[LedgerKey],
297 ) -> Result<GetLedgerEntriesResponse, ProviderError>;
298 async fn get_events(
299 &self,
300 request: GetEventsRequest,
301 ) -> Result<GetEventsResponse, ProviderError>;
302 async fn raw_request_dyn(
303 &self,
304 method: &str,
305 params: serde_json::Value,
306 id: Option<JsonRpcId>,
307 ) -> Result<serde_json::Value, ProviderError>;
308}
309
310impl StellarProvider {
311 pub fn new(
313 mut rpc_configs: Vec<RpcConfig>,
314 timeout_seconds: u64,
315 ) -> Result<Self, ProviderError> {
316 if rpc_configs.is_empty() {
317 return Err(ProviderError::NetworkConfiguration(
318 "No RPC configurations provided for StellarProvider".to_string(),
319 ));
320 }
321
322 RpcConfig::validate_list(&rpc_configs)
323 .map_err(|e| ProviderError::NetworkConfiguration(e.to_string()))?;
324
325 rpc_configs.retain(|config| config.get_weight() > 0);
326
327 if rpc_configs.is_empty() {
328 return Err(ProviderError::NetworkConfiguration(
329 "No active RPC configurations provided (all weights are 0 or list was empty after filtering)".to_string(),
330 ));
331 }
332
333 let selector = RpcSelector::new(rpc_configs).map_err(|e| {
334 ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {e}"))
335 })?;
336
337 let retry_config = RetryConfig::from_env();
338
339 Ok(Self {
340 selector,
341 timeout_seconds: Duration::from_secs(timeout_seconds),
342 retry_config,
343 })
344 }
345
346 fn initialize_provider(&self, url: &str) -> Result<Client, ProviderError> {
348 Client::new(url).map_err(|e| {
349 ProviderError::NetworkConfiguration(format!(
350 "Failed to create Stellar RPC client: {e} - URL: '{url}'"
351 ))
352 })
353 }
354
355 fn initialize_raw_provider(&self, url: &str) -> Result<ReqwestClient, ProviderError> {
359 ReqwestClient::builder()
360 .timeout(self.timeout_seconds)
361 .build()
362 .map_err(|e| {
363 ProviderError::NetworkConfiguration(format!(
364 "Failed to create HTTP client for raw RPC: {e} - URL: '{url}'"
365 ))
366 })
367 }
368
369 async fn retry_rpc_call<T, F, Fut>(
371 &self,
372 operation_name: &str,
373 operation: F,
374 ) -> Result<T, ProviderError>
375 where
376 F: Fn(Client) -> Fut,
377 Fut: std::future::Future<Output = Result<T, ProviderError>>,
378 {
379 let provider_url_raw = match self.selector.get_current_url() {
380 Ok(url) => url,
381 Err(e) => {
382 return Err(ProviderError::NetworkConfiguration(format!(
383 "No RPC URL available for StellarProvider: {e}"
384 )));
385 }
386 };
387 let provider_url = normalize_url_for_log(&provider_url_raw);
388
389 tracing::debug!(
390 "Starting Stellar RPC operation '{}' with timeout: {}s, provider_url: {}",
391 operation_name,
392 self.timeout_seconds.as_secs(),
393 provider_url
394 );
395
396 retry_rpc_call(
397 &self.selector,
398 operation_name,
399 is_retriable_error,
400 should_mark_provider_failed,
401 |url| self.initialize_provider(url),
402 operation,
403 Some(self.retry_config.clone()),
404 )
405 .await
406 }
407
408 async fn retry_raw_request(
410 &self,
411 operation_name: &str,
412 request: serde_json::Value,
413 ) -> Result<serde_json::Value, ProviderError> {
414 let provider_url_raw = match self.selector.get_current_url() {
415 Ok(url) => url,
416 Err(e) => {
417 return Err(ProviderError::NetworkConfiguration(format!(
418 "No RPC URL available for StellarProvider: {e}"
419 )));
420 }
421 };
422 let provider_url = normalize_url_for_log(&provider_url_raw);
423
424 tracing::debug!(
425 "Starting raw RPC operation '{}' with timeout: {}s, provider_url: {}",
426 operation_name,
427 self.timeout_seconds.as_secs(),
428 provider_url
429 );
430
431 let request_clone = request.clone();
432 retry_rpc_call(
433 &self.selector,
434 operation_name,
435 is_retriable_error,
436 should_mark_provider_failed,
437 |url| {
438 self.initialize_raw_provider(url)
440 .map(|client| (url.to_string(), client))
441 },
442 |(url, client): (String, ReqwestClient)| {
443 let request_for_call = request_clone.clone();
444 async move {
445 let response = client
446 .post(&url)
447 .json(&request_for_call)
448 .timeout(self.timeout_seconds)
450 .send()
451 .await
452 .map_err(ProviderError::from)?;
453
454 let json_response: serde_json::Value =
455 response.json().await.map_err(ProviderError::from)?;
456
457 Ok(json_response)
458 }
459 },
460 Some(self.retry_config.clone()),
461 )
462 .await
463 }
464}
465
466#[async_trait]
467impl StellarProviderTrait for StellarProvider {
468 async fn get_account(&self, account_id: &str) -> Result<AccountEntry, ProviderError> {
469 let account_id = Arc::new(account_id.to_string());
470
471 self.retry_rpc_call("get_account", move |client| {
472 let account_id = Arc::clone(&account_id);
473 async move {
474 client.get_account(&account_id).await.map_err(|e| {
475 categorize_stellar_error_with_context(e, Some("Failed to get account"))
476 })
477 }
478 })
479 .await
480 }
481
482 async fn simulate_transaction_envelope(
483 &self,
484 tx_envelope: &TransactionEnvelope,
485 ) -> Result<SimulateTransactionResponse, ProviderError> {
486 let tx_envelope = Arc::new(tx_envelope.clone());
487
488 self.retry_rpc_call("simulate_transaction_envelope", move |client| {
489 let tx_envelope = Arc::clone(&tx_envelope);
490 async move {
491 client
492 .simulate_transaction_envelope(&tx_envelope, None)
493 .await
494 .map_err(|e| {
495 categorize_stellar_error_with_context(
496 e,
497 Some("Failed to simulate transaction"),
498 )
499 })
500 }
501 })
502 .await
503 }
504
505 async fn send_transaction_polling(
506 &self,
507 tx_envelope: &TransactionEnvelope,
508 ) -> Result<SorobanTransactionResponse, ProviderError> {
509 let tx_envelope = Arc::new(tx_envelope.clone());
510
511 self.retry_rpc_call("send_transaction_polling", move |client| {
512 let tx_envelope = Arc::clone(&tx_envelope);
513 async move {
514 client
515 .send_transaction_polling(&tx_envelope)
516 .await
517 .map(SorobanTransactionResponse::from)
518 .map_err(|e| {
519 categorize_stellar_error_with_context(
520 e,
521 Some("Failed to send transaction (polling)"),
522 )
523 })
524 }
525 })
526 .await
527 }
528
529 async fn get_network(&self) -> Result<GetNetworkResponse, ProviderError> {
530 self.retry_rpc_call("get_network", |client| async move {
531 client.get_network().await.map_err(|e| {
532 categorize_stellar_error_with_context(e, Some("Failed to get network"))
533 })
534 })
535 .await
536 }
537
538 async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse, ProviderError> {
539 self.retry_rpc_call("get_latest_ledger", |client| async move {
540 client.get_latest_ledger().await.map_err(|e| {
541 categorize_stellar_error_with_context(e, Some("Failed to get latest ledger"))
542 })
543 })
544 .await
545 }
546
547 async fn send_transaction(
548 &self,
549 tx_envelope: &TransactionEnvelope,
550 ) -> Result<Hash, ProviderError> {
551 let tx_envelope = Arc::new(tx_envelope.clone());
552
553 self.retry_rpc_call("send_transaction", move |client| {
554 let tx_envelope = Arc::clone(&tx_envelope);
555 async move {
556 client.send_transaction(&tx_envelope).await.map_err(|e| {
557 categorize_stellar_error_with_context(e, Some("Failed to send transaction"))
558 })
559 }
560 })
561 .await
562 }
563
564 async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse, ProviderError> {
565 let tx_id = Arc::new(tx_id.clone());
566
567 self.retry_rpc_call("get_transaction", move |client| {
568 let tx_id = Arc::clone(&tx_id);
569 async move {
570 client.get_transaction(&tx_id).await.map_err(|e| {
571 categorize_stellar_error_with_context(e, Some("Failed to get transaction"))
572 })
573 }
574 })
575 .await
576 }
577
578 async fn get_transactions(
579 &self,
580 request: GetTransactionsRequest,
581 ) -> Result<GetTransactionsResponse, ProviderError> {
582 let request = Arc::new(request);
583
584 self.retry_rpc_call("get_transactions", move |client| {
585 let request = Arc::clone(&request);
586 async move {
587 client
588 .get_transactions((*request).clone())
589 .await
590 .map_err(|e| {
591 categorize_stellar_error_with_context(e, Some("Failed to get transactions"))
592 })
593 }
594 })
595 .await
596 }
597
598 async fn get_ledger_entries(
599 &self,
600 keys: &[LedgerKey],
601 ) -> Result<GetLedgerEntriesResponse, ProviderError> {
602 let keys = Arc::new(keys.to_vec());
603
604 self.retry_rpc_call("get_ledger_entries", move |client| {
605 let keys = Arc::clone(&keys);
606 async move {
607 client.get_ledger_entries(&keys).await.map_err(|e| {
608 categorize_stellar_error_with_context(e, Some("Failed to get ledger entries"))
609 })
610 }
611 })
612 .await
613 }
614
615 async fn get_events(
616 &self,
617 request: GetEventsRequest,
618 ) -> Result<GetEventsResponse, ProviderError> {
619 let request = Arc::new(request);
620
621 self.retry_rpc_call("get_events", move |client| {
622 let request = Arc::clone(&request);
623 async move {
624 client
625 .get_events(
626 request.start.clone(),
627 request.event_type,
628 &request.contract_ids,
629 &request.topics,
630 request.limit,
631 )
632 .await
633 .map_err(|e| {
634 categorize_stellar_error_with_context(e, Some("Failed to get events"))
635 })
636 }
637 })
638 .await
639 }
640
641 async fn raw_request_dyn(
642 &self,
643 method: &str,
644 params: serde_json::Value,
645 id: Option<JsonRpcId>,
646 ) -> Result<serde_json::Value, ProviderError> {
647 let id_value = match id {
648 Some(id) => serde_json::to_value(id)
649 .map_err(|e| ProviderError::Other(format!("Failed to serialize id: {e}")))?,
650 None => serde_json::json!(generate_unique_rpc_id()),
651 };
652
653 let request = serde_json::json!({
654 "jsonrpc": "2.0",
655 "id": id_value,
656 "method": method,
657 "params": params,
658 });
659
660 let response = self.retry_raw_request("raw_request_dyn", request).await?;
661
662 if let Some(error) = response.get("error") {
664 if let Some(code) = error.get("code").and_then(|c| c.as_i64()) {
665 return Err(ProviderError::RpcErrorCode {
666 code,
667 message: error
668 .get("message")
669 .and_then(|m| m.as_str())
670 .unwrap_or("Unknown error")
671 .to_string(),
672 });
673 }
674 return Err(ProviderError::Other(format!("JSON-RPC error: {error}")));
675 }
676
677 response
679 .get("result")
680 .cloned()
681 .ok_or_else(|| ProviderError::Other("No result field in JSON-RPC response".to_string()))
682 }
683}
684
685#[cfg(test)]
686mod stellar_rpc_tests {
687 use super::*;
688 use crate::services::provider::stellar::{
689 GetEventsRequest, StellarProvider, StellarProviderTrait,
690 };
691 use futures::FutureExt;
692 use lazy_static::lazy_static;
693 use mockall::predicate as p;
694 use soroban_rs::stellar_rpc_client::{
695 EventStart, GetEventsResponse, GetLatestLedgerResponse, GetLedgerEntriesResponse,
696 GetNetworkResponse, GetTransactionEvents, GetTransactionResponse, GetTransactionsRequest,
697 GetTransactionsResponse, SimulateTransactionResponse,
698 };
699 use soroban_rs::xdr::{
700 AccountEntryExt, Hash, LedgerKey, OperationResult, String32, Thresholds,
701 TransactionEnvelope, TransactionResult, TransactionResultExt, TransactionResultResult,
702 VecM,
703 };
704 use soroban_rs::{create_mock_set_options_tx_envelope, SorobanTransactionResponse};
705 use std::str::FromStr;
706 use std::sync::Mutex;
707
708 lazy_static! {
709 static ref STELLAR_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
710 }
711
712 struct StellarTestEnvGuard {
713 _mutex_guard: std::sync::MutexGuard<'static, ()>,
714 }
715
716 impl StellarTestEnvGuard {
717 fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
718 std::env::set_var(
719 "API_KEY",
720 "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
721 );
722 std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
723 std::env::set_var("PROVIDER_MAX_RETRIES", "1");
725 std::env::set_var("PROVIDER_MAX_FAILOVERS", "0");
726 std::env::set_var("PROVIDER_RETRY_BASE_DELAY_MS", "0");
727 std::env::set_var("PROVIDER_RETRY_MAX_DELAY_MS", "0");
728
729 Self {
730 _mutex_guard: mutex_guard,
731 }
732 }
733 }
734
735 impl Drop for StellarTestEnvGuard {
736 fn drop(&mut self) {
737 std::env::remove_var("API_KEY");
738 std::env::remove_var("REDIS_URL");
739 std::env::remove_var("PROVIDER_MAX_RETRIES");
740 std::env::remove_var("PROVIDER_MAX_FAILOVERS");
741 std::env::remove_var("PROVIDER_RETRY_BASE_DELAY_MS");
742 std::env::remove_var("PROVIDER_RETRY_MAX_DELAY_MS");
743 }
744 }
745
746 fn setup_test_env() -> StellarTestEnvGuard {
748 let guard = STELLAR_TEST_ENV_MUTEX
749 .lock()
750 .unwrap_or_else(|e| e.into_inner());
751 StellarTestEnvGuard::new(guard)
752 }
753
754 fn dummy_hash() -> Hash {
755 Hash([0u8; 32])
756 }
757
758 fn dummy_get_network_response() -> GetNetworkResponse {
759 GetNetworkResponse {
760 friendbot_url: Some("https://friendbot.testnet.stellar.org/".into()),
761 passphrase: "Test SDF Network ; September 2015".into(),
762 protocol_version: 20,
763 }
764 }
765
766 fn dummy_get_latest_ledger_response() -> GetLatestLedgerResponse {
767 GetLatestLedgerResponse {
768 id: "c73c5eac58a441d4eb733c35253ae85f783e018f7be5ef974258fed067aabb36".into(),
769 protocol_version: 20,
770 sequence: 2_539_605,
771 }
772 }
773
774 fn dummy_simulate() -> SimulateTransactionResponse {
775 SimulateTransactionResponse {
776 min_resource_fee: 100,
777 transaction_data: "test".to_string(),
778 ..Default::default()
779 }
780 }
781
782 fn create_success_tx_result() -> TransactionResult {
783 let empty_vec: Vec<OperationResult> = Vec::new();
785 let op_results = empty_vec.try_into().unwrap_or_default();
786
787 TransactionResult {
788 fee_charged: 100,
789 result: TransactionResultResult::TxSuccess(op_results),
790 ext: TransactionResultExt::V0,
791 }
792 }
793
794 fn dummy_get_transaction_response() -> GetTransactionResponse {
795 GetTransactionResponse {
796 status: "SUCCESS".to_string(),
797 envelope: None,
798 result: Some(create_success_tx_result()),
799 result_meta: None,
800 events: GetTransactionEvents {
801 contract_events: vec![],
802 diagnostic_events: vec![],
803 transaction_events: vec![],
804 },
805 ledger: None,
806 }
807 }
808
809 fn dummy_soroban_tx() -> SorobanTransactionResponse {
810 SorobanTransactionResponse {
811 response: dummy_get_transaction_response(),
812 }
813 }
814
815 fn dummy_get_transactions_response() -> GetTransactionsResponse {
816 GetTransactionsResponse {
817 transactions: vec![],
818 latest_ledger: 0,
819 latest_ledger_close_time: 0,
820 oldest_ledger: 0,
821 oldest_ledger_close_time: 0,
822 cursor: 0,
823 }
824 }
825
826 fn dummy_get_ledger_entries_response() -> GetLedgerEntriesResponse {
827 GetLedgerEntriesResponse {
828 entries: None,
829 latest_ledger: 0,
830 }
831 }
832
833 fn dummy_get_events_response() -> GetEventsResponse {
834 GetEventsResponse {
835 events: vec![],
836 latest_ledger: 0,
837 latest_ledger_close_time: "0".to_string(),
838 oldest_ledger: 0,
839 oldest_ledger_close_time: "0".to_string(),
840 cursor: "0".to_string(),
841 }
842 }
843
844 fn dummy_transaction_envelope() -> TransactionEnvelope {
845 create_mock_set_options_tx_envelope()
846 }
847
848 fn dummy_ledger_key() -> LedgerKey {
849 LedgerKey::Account(LedgerKeyAccount {
850 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
851 })
852 }
853
854 pub fn mock_account_entry(account_id: &str) -> AccountEntry {
855 AccountEntry {
856 account_id: AccountId(PublicKey::from_str(account_id).unwrap()),
857 balance: 0,
858 ext: AccountEntryExt::V0,
859 flags: 0,
860 home_domain: String32::default(),
861 inflation_dest: None,
862 seq_num: 0.into(),
863 num_sub_entries: 0,
864 signers: VecM::default(),
865 thresholds: Thresholds([0, 0, 0, 0]),
866 }
867 }
868
869 fn dummy_account_entry() -> AccountEntry {
870 mock_account_entry("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
871 }
872
873 #[test]
878 fn test_new_provider() {
879 let _env_guard = setup_test_env();
880
881 let provider =
882 StellarProvider::new(vec![RpcConfig::new("http://localhost:8000".to_string())], 0);
883 assert!(provider.is_ok());
884
885 let provider_err = StellarProvider::new(vec![], 0);
886 assert!(provider_err.is_err());
887 match provider_err.unwrap_err() {
888 ProviderError::NetworkConfiguration(msg) => {
889 assert!(msg.contains("No RPC configurations provided"));
890 }
891 _ => panic!("Unexpected error type"),
892 }
893 }
894
895 #[test]
896 fn test_new_provider_selects_highest_weight() {
897 let _env_guard = setup_test_env();
898
899 let configs = vec![
900 RpcConfig::with_weight("http://rpc1.example.com".to_string(), 10).unwrap(),
901 RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), RpcConfig::with_weight("http://rpc3.example.com".to_string(), 50).unwrap(),
903 ];
904 let provider = StellarProvider::new(configs, 0);
905 assert!(provider.is_ok());
906 }
910
911 #[test]
912 fn test_new_provider_ignores_weight_zero() {
913 let _env_guard = setup_test_env();
914
915 let configs = vec![
916 RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(), RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), ];
919 let provider = StellarProvider::new(configs, 0);
920 assert!(provider.is_ok());
921
922 let configs_only_zero =
923 vec![RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap()];
924 let provider_err = StellarProvider::new(configs_only_zero, 0);
925 assert!(provider_err.is_err());
926 match provider_err.unwrap_err() {
927 ProviderError::NetworkConfiguration(msg) => {
928 assert!(msg.contains("No active RPC configurations provided"));
929 }
930 _ => panic!("Unexpected error type"),
931 }
932 }
933
934 #[test]
935 fn test_new_provider_invalid_url_scheme() {
936 let configs = vec![RpcConfig::new("ftp://invalid.example.com".to_string())];
937 let provider_err = StellarProvider::new(configs, 0);
938 assert!(provider_err.is_err());
939 match provider_err.unwrap_err() {
940 ProviderError::NetworkConfiguration(msg) => {
941 assert!(msg.contains("Invalid URL scheme"));
942 }
943 _ => panic!("Unexpected error type"),
944 }
945 }
946
947 #[test]
948 fn test_new_provider_all_zero_weight_configs() {
949 let _env_guard = setup_test_env();
950
951 let configs = vec![
952 RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(),
953 RpcConfig::with_weight("http://rpc2.example.com".to_string(), 0).unwrap(),
954 ];
955 let provider_err = StellarProvider::new(configs, 0);
956 assert!(provider_err.is_err());
957 match provider_err.unwrap_err() {
958 ProviderError::NetworkConfiguration(msg) => {
959 assert!(msg.contains("No active RPC configurations provided"));
960 }
961 _ => panic!("Unexpected error type"),
962 }
963 }
964
965 #[tokio::test]
966 async fn test_mock_basic_methods() {
967 let mut mock = MockStellarProviderTrait::new();
968
969 mock.expect_get_network()
970 .times(1)
971 .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
972
973 mock.expect_get_latest_ledger()
974 .times(1)
975 .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
976
977 assert!(mock.get_network().await.is_ok());
978 assert!(mock.get_latest_ledger().await.is_ok());
979 }
980
981 #[tokio::test]
982 async fn test_mock_transaction_flow() {
983 let mut mock = MockStellarProviderTrait::new();
984
985 let envelope: TransactionEnvelope = dummy_transaction_envelope();
986 let hash = dummy_hash();
987
988 mock.expect_simulate_transaction_envelope()
989 .withf(|_| true)
990 .times(1)
991 .returning(|_| async { Ok(dummy_simulate()) }.boxed());
992
993 mock.expect_send_transaction()
994 .withf(|_| true)
995 .times(1)
996 .returning(|_| async { Ok(dummy_hash()) }.boxed());
997
998 mock.expect_send_transaction_polling()
999 .withf(|_| true)
1000 .times(1)
1001 .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
1002
1003 mock.expect_get_transaction()
1004 .withf(|_| true)
1005 .times(1)
1006 .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
1007
1008 mock.simulate_transaction_envelope(&envelope).await.unwrap();
1009 mock.send_transaction(&envelope).await.unwrap();
1010 mock.send_transaction_polling(&envelope).await.unwrap();
1011 mock.get_transaction(&hash).await.unwrap();
1012 }
1013
1014 #[tokio::test]
1015 async fn test_mock_events_and_entries() {
1016 let mut mock = MockStellarProviderTrait::new();
1017
1018 mock.expect_get_events()
1019 .times(1)
1020 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1021
1022 mock.expect_get_ledger_entries()
1023 .times(1)
1024 .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
1025
1026 let events_request = GetEventsRequest {
1027 start: EventStart::Ledger(1),
1028 event_type: None,
1029 contract_ids: vec![],
1030 topics: vec![],
1031 limit: Some(10),
1032 };
1033
1034 let dummy_key: LedgerKey = dummy_ledger_key();
1035 mock.get_events(events_request).await.unwrap();
1036 mock.get_ledger_entries(&[dummy_key]).await.unwrap();
1037 }
1038
1039 #[tokio::test]
1040 async fn test_mock_all_methods_ok() {
1041 let mut mock = MockStellarProviderTrait::new();
1042
1043 mock.expect_get_account()
1044 .with(p::eq("GTESTACCOUNTID"))
1045 .times(1)
1046 .returning(|_| async { Ok(dummy_account_entry()) }.boxed());
1047
1048 mock.expect_simulate_transaction_envelope()
1049 .times(1)
1050 .returning(|_| async { Ok(dummy_simulate()) }.boxed());
1051
1052 mock.expect_send_transaction_polling()
1053 .times(1)
1054 .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
1055
1056 mock.expect_get_network()
1057 .times(1)
1058 .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
1059
1060 mock.expect_get_latest_ledger()
1061 .times(1)
1062 .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
1063
1064 mock.expect_send_transaction()
1065 .times(1)
1066 .returning(|_| async { Ok(dummy_hash()) }.boxed());
1067
1068 mock.expect_get_transaction()
1069 .times(1)
1070 .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
1071
1072 mock.expect_get_transactions()
1073 .times(1)
1074 .returning(|_| async { Ok(dummy_get_transactions_response()) }.boxed());
1075
1076 mock.expect_get_ledger_entries()
1077 .times(1)
1078 .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
1079
1080 mock.expect_get_events()
1081 .times(1)
1082 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1083
1084 let _ = mock.get_account("GTESTACCOUNTID").await.unwrap();
1085 let env: TransactionEnvelope = dummy_transaction_envelope();
1086 mock.simulate_transaction_envelope(&env).await.unwrap();
1087 mock.send_transaction_polling(&env).await.unwrap();
1088 mock.get_network().await.unwrap();
1089 mock.get_latest_ledger().await.unwrap();
1090 mock.send_transaction(&env).await.unwrap();
1091
1092 let h = dummy_hash();
1093 mock.get_transaction(&h).await.unwrap();
1094
1095 let req: GetTransactionsRequest = GetTransactionsRequest {
1096 start_ledger: None,
1097 pagination: None,
1098 };
1099 mock.get_transactions(req).await.unwrap();
1100
1101 let key: LedgerKey = dummy_ledger_key();
1102 mock.get_ledger_entries(&[key]).await.unwrap();
1103
1104 let ev_req = GetEventsRequest {
1105 start: EventStart::Ledger(0),
1106 event_type: None,
1107 contract_ids: vec![],
1108 topics: vec![],
1109 limit: None,
1110 };
1111 mock.get_events(ev_req).await.unwrap();
1112 }
1113
1114 #[tokio::test]
1115 async fn test_error_propagation() {
1116 let mut mock = MockStellarProviderTrait::new();
1117
1118 mock.expect_get_account()
1119 .returning(|_| async { Err(ProviderError::Other("boom".to_string())) }.boxed());
1120
1121 let res = mock.get_account("BAD").await;
1122 assert!(res.is_err());
1123 assert!(res.unwrap_err().to_string().contains("boom"));
1124 }
1125
1126 #[tokio::test]
1127 async fn test_get_events_edge_cases() {
1128 let mut mock = MockStellarProviderTrait::new();
1129
1130 mock.expect_get_events()
1131 .withf(|req| {
1132 req.contract_ids.is_empty() && req.topics.is_empty() && req.limit.is_none()
1133 })
1134 .times(1)
1135 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1136
1137 let ev_req = GetEventsRequest {
1138 start: EventStart::Ledger(0),
1139 event_type: None,
1140 contract_ids: vec![],
1141 topics: vec![],
1142 limit: None,
1143 };
1144
1145 mock.get_events(ev_req).await.unwrap();
1146 }
1147
1148 #[test]
1149 fn test_provider_send_sync_bounds() {
1150 fn assert_send_sync<T: Send + Sync>() {}
1151 assert_send_sync::<StellarProvider>();
1152 }
1153
1154 #[cfg(test)]
1155 mod concrete_tests {
1156 use super::*;
1157
1158 const NON_EXISTENT_URL: &str = "http://127.0.0.1:9998";
1159
1160 fn setup_provider() -> StellarProvider {
1161 StellarProvider::new(vec![RpcConfig::new(NON_EXISTENT_URL.to_string())], 0)
1162 .expect("Provider creation should succeed even with bad URL")
1163 }
1164
1165 #[tokio::test]
1166 async fn test_concrete_get_account_error() {
1167 let _env_guard = setup_test_env();
1168 let provider = setup_provider();
1169 let result = provider.get_account("SOME_ACCOUNT_ID").await;
1170 assert!(result.is_err());
1171 let err_str = result.unwrap_err().to_string();
1172 assert!(
1174 err_str.contains("Failed to get account"),
1175 "Unexpected error message: {}",
1176 err_str
1177 );
1178 }
1179
1180 #[tokio::test]
1181 async fn test_concrete_simulate_transaction_envelope_error() {
1182 let _env_guard = setup_test_env();
1183
1184 let provider = setup_provider();
1185 let envelope: TransactionEnvelope = dummy_transaction_envelope();
1186 let result = provider.simulate_transaction_envelope(&envelope).await;
1187 assert!(result.is_err());
1188 let err_str = result.unwrap_err().to_string();
1189 assert!(
1191 err_str.contains("Failed to simulate transaction"),
1192 "Unexpected error message: {}",
1193 err_str
1194 );
1195 }
1196
1197 #[tokio::test]
1198 async fn test_concrete_send_transaction_polling_error() {
1199 let _env_guard = setup_test_env();
1200
1201 let provider = setup_provider();
1202 let envelope: TransactionEnvelope = dummy_transaction_envelope();
1203 let result = provider.send_transaction_polling(&envelope).await;
1204 assert!(result.is_err());
1205 let err_str = result.unwrap_err().to_string();
1206 assert!(
1208 err_str.contains("Failed to send transaction (polling)"),
1209 "Unexpected error message: {}",
1210 err_str
1211 );
1212 }
1213
1214 #[tokio::test]
1215 async fn test_concrete_get_network_error() {
1216 let _env_guard = setup_test_env();
1217
1218 let provider = setup_provider();
1219 let result = provider.get_network().await;
1220 assert!(result.is_err());
1221 let err_str = result.unwrap_err().to_string();
1222 assert!(
1224 err_str.contains("Failed to get network"),
1225 "Unexpected error message: {}",
1226 err_str
1227 );
1228 }
1229
1230 #[tokio::test]
1231 async fn test_concrete_get_latest_ledger_error() {
1232 let _env_guard = setup_test_env();
1233
1234 let provider = setup_provider();
1235 let result = provider.get_latest_ledger().await;
1236 assert!(result.is_err());
1237 let err_str = result.unwrap_err().to_string();
1238 assert!(
1240 err_str.contains("Failed to get latest ledger"),
1241 "Unexpected error message: {}",
1242 err_str
1243 );
1244 }
1245
1246 #[tokio::test]
1247 async fn test_concrete_send_transaction_error() {
1248 let _env_guard = setup_test_env();
1249
1250 let provider = setup_provider();
1251 let envelope: TransactionEnvelope = dummy_transaction_envelope();
1252 let result = provider.send_transaction(&envelope).await;
1253 assert!(result.is_err());
1254 let err_str = result.unwrap_err().to_string();
1255 assert!(
1257 err_str.contains("Failed to send transaction"),
1258 "Unexpected error message: {}",
1259 err_str
1260 );
1261 }
1262
1263 #[tokio::test]
1264 async fn test_concrete_get_transaction_error() {
1265 let _env_guard = setup_test_env();
1266
1267 let provider = setup_provider();
1268 let hash: Hash = dummy_hash();
1269 let result = provider.get_transaction(&hash).await;
1270 assert!(result.is_err());
1271 let err_str = result.unwrap_err().to_string();
1272 assert!(
1274 err_str.contains("Failed to get transaction"),
1275 "Unexpected error message: {}",
1276 err_str
1277 );
1278 }
1279
1280 #[tokio::test]
1281 async fn test_concrete_get_transactions_error() {
1282 let _env_guard = setup_test_env();
1283
1284 let provider = setup_provider();
1285 let req = GetTransactionsRequest {
1286 start_ledger: None,
1287 pagination: None,
1288 };
1289 let result = provider.get_transactions(req).await;
1290 assert!(result.is_err());
1291 let err_str = result.unwrap_err().to_string();
1292 assert!(
1294 err_str.contains("Failed to get transactions"),
1295 "Unexpected error message: {}",
1296 err_str
1297 );
1298 }
1299
1300 #[tokio::test]
1301 async fn test_concrete_get_ledger_entries_error() {
1302 let _env_guard = setup_test_env();
1303
1304 let provider = setup_provider();
1305 let key: LedgerKey = dummy_ledger_key();
1306 let result = provider.get_ledger_entries(&[key]).await;
1307 assert!(result.is_err());
1308 let err_str = result.unwrap_err().to_string();
1309 assert!(
1311 err_str.contains("Failed to get ledger entries"),
1312 "Unexpected error message: {}",
1313 err_str
1314 );
1315 }
1316
1317 #[tokio::test]
1318 async fn test_concrete_get_events_error() {
1319 let _env_guard = setup_test_env();
1320 let provider = setup_provider();
1321 let req = GetEventsRequest {
1322 start: EventStart::Ledger(1),
1323 event_type: None,
1324 contract_ids: vec![],
1325 topics: vec![],
1326 limit: None,
1327 };
1328 let result = provider.get_events(req).await;
1329 assert!(result.is_err());
1330 let err_str = result.unwrap_err().to_string();
1331 assert!(
1333 err_str.contains("Failed to get events"),
1334 "Unexpected error message: {}",
1335 err_str
1336 );
1337 }
1338 }
1339
1340 #[test]
1341 fn test_generate_unique_rpc_id() {
1342 let id1 = generate_unique_rpc_id();
1343 let id2 = generate_unique_rpc_id();
1344 assert_ne!(id1, id2, "Generated IDs should be unique");
1345 assert!(id1 > 0, "ID should be positive");
1346 assert!(id2 > 0, "ID should be positive");
1347 assert!(id2 > id1, "IDs should be monotonically increasing");
1348 }
1349
1350 #[test]
1351 fn test_normalize_url_for_log() {
1352 assert_eq!(
1354 normalize_url_for_log("https://api.example.com/path"),
1355 "https://api.example.com/path"
1356 );
1357
1358 assert_eq!(
1360 normalize_url_for_log("https://api.example.com/path?api_key=secret&other=value"),
1361 "https://api.example.com/path"
1362 );
1363
1364 assert_eq!(
1366 normalize_url_for_log("https://api.example.com/path#section"),
1367 "https://api.example.com/path"
1368 );
1369
1370 assert_eq!(
1372 normalize_url_for_log("https://api.example.com/path?key=value#fragment"),
1373 "https://api.example.com/path"
1374 );
1375
1376 assert_eq!(
1378 normalize_url_for_log("https://user:password@api.example.com/path"),
1379 "https://<redacted>@api.example.com/path"
1380 );
1381
1382 assert_eq!(
1384 normalize_url_for_log("https://user:pass@api.example.com/path?token=abc#frag"),
1385 "https://<redacted>@api.example.com/path"
1386 );
1387
1388 assert_eq!(
1390 normalize_url_for_log("https://api.example.com/path?token=abc"),
1391 "https://api.example.com/path"
1392 );
1393
1394 assert_eq!(normalize_url_for_log("not-a-url"), "not-a-url");
1396 }
1397
1398 #[test]
1399 fn test_categorize_stellar_error_with_context_timeout() {
1400 let err = StellarClientError::TransactionSubmissionTimeout;
1401 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1402 assert!(matches!(result, ProviderError::Timeout));
1403 }
1404
1405 #[test]
1406 fn test_categorize_stellar_error_with_context_xdr_error() {
1407 use soroban_rs::xdr::Error as XdrError;
1408 let err = StellarClientError::Xdr(XdrError::Invalid);
1409 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1410 match result {
1411 ProviderError::Other(msg) => {
1412 assert!(msg.contains("Test operation"));
1413 }
1414 _ => panic!("Expected Other error"),
1415 }
1416 }
1417
1418 #[test]
1419 fn test_categorize_stellar_error_with_context_serde_error() {
1420 let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
1422 let err = StellarClientError::Serde(json_err);
1423 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1424 match result {
1425 ProviderError::Other(msg) => {
1426 assert!(msg.contains("Test operation"));
1427 }
1428 _ => panic!("Expected Other error"),
1429 }
1430 }
1431
1432 #[test]
1433 fn test_categorize_stellar_error_with_context_url_errors() {
1434 let invalid_uri_err: http::uri::InvalidUri =
1436 ":::invalid url".parse::<http::Uri>().unwrap_err();
1437 let err = StellarClientError::InvalidRpcUrl(invalid_uri_err);
1438 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1439 match result {
1440 ProviderError::NetworkConfiguration(msg) => {
1441 assert!(msg.contains("Test operation"));
1442 assert!(msg.contains("Invalid RPC URL"));
1443 }
1444 _ => panic!("Expected NetworkConfiguration error"),
1445 }
1446
1447 let err = StellarClientError::InvalidUrl("not a url".to_string());
1449 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1450 match result {
1451 ProviderError::NetworkConfiguration(msg) => {
1452 assert!(msg.contains("Test operation"));
1453 assert!(msg.contains("Invalid URL"));
1454 }
1455 _ => panic!("Expected NetworkConfiguration error"),
1456 }
1457 }
1458
1459 #[test]
1460 fn test_categorize_stellar_error_with_context_network_passphrase() {
1461 let err = StellarClientError::InvalidNetworkPassphrase {
1462 expected: "Expected".to_string(),
1463 server: "Server".to_string(),
1464 };
1465 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1466 match result {
1467 ProviderError::NetworkConfiguration(msg) => {
1468 assert!(msg.contains("Test operation"));
1469 assert!(msg.contains("Expected"));
1470 assert!(msg.contains("Server"));
1471 }
1472 _ => panic!("Expected NetworkConfiguration error"),
1473 }
1474 }
1475
1476 #[test]
1477 fn test_categorize_stellar_error_with_context_json_rpc_call_error() {
1478 let err = StellarClientError::TransactionSubmissionTimeout;
1482 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1483 assert!(matches!(result, ProviderError::Timeout));
1485 }
1486
1487 #[test]
1488 fn test_categorize_stellar_error_with_context_json_rpc_timeout() {
1489 let err = StellarClientError::TransactionSubmissionTimeout;
1491 let result = categorize_stellar_error_with_context(err, None);
1492 assert!(matches!(result, ProviderError::Timeout));
1493 }
1494
1495 #[test]
1496 fn test_categorize_stellar_error_with_context_transport_errors() {
1497 let err = StellarClientError::InvalidResponse;
1499 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1500 match result {
1501 ProviderError::Other(msg) => {
1502 assert!(msg.contains("Test operation"));
1503 assert!(msg.contains("Invalid response"));
1504 }
1505 _ => panic!("Expected Other error for response issues"),
1506 }
1507 }
1508
1509 #[test]
1510 fn test_categorize_stellar_error_with_context_response_errors() {
1511 let err = StellarClientError::InvalidResponse;
1513 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1514 match result {
1515 ProviderError::Other(msg) => {
1516 assert!(msg.contains("Test operation"));
1517 assert!(msg.contains("Invalid response"));
1518 }
1519 _ => panic!("Expected Other error"),
1520 }
1521
1522 let err = StellarClientError::MissingResult;
1524 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1525 match result {
1526 ProviderError::Other(msg) => {
1527 assert!(msg.contains("Test operation"));
1528 assert!(msg.contains("Missing result"));
1529 }
1530 _ => panic!("Expected Other error"),
1531 }
1532 }
1533
1534 #[test]
1535 fn test_categorize_stellar_error_with_context_transaction_errors() {
1536 let err = StellarClientError::TransactionFailed("tx failed".to_string());
1538 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1539 match result {
1540 ProviderError::Other(msg) => {
1541 assert!(msg.contains("Test operation"));
1542 assert!(msg.contains("tx failed"));
1543 }
1544 _ => panic!("Expected Other error"),
1545 }
1546
1547 let err = StellarClientError::NotFound("Account".to_string(), "123".to_string());
1549 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1550 match result {
1551 ProviderError::Other(msg) => {
1552 assert!(msg.contains("Test operation"));
1553 assert!(msg.contains("Account not found"));
1554 assert!(msg.contains("123"));
1555 }
1556 _ => panic!("Expected Other error"),
1557 }
1558 }
1559
1560 #[test]
1561 fn test_categorize_stellar_error_with_context_validation_errors() {
1562 let err = StellarClientError::InvalidCursor;
1564 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1565 match result {
1566 ProviderError::Other(msg) => {
1567 assert!(msg.contains("Test operation"));
1568 assert!(msg.contains("Invalid cursor"));
1569 }
1570 _ => panic!("Expected Other error"),
1571 }
1572
1573 let err = StellarClientError::LargeFee(1000000);
1575 let result = categorize_stellar_error_with_context(err, Some("Test operation"));
1576 match result {
1577 ProviderError::Other(msg) => {
1578 assert!(msg.contains("Test operation"));
1579 assert!(msg.contains("1000000"));
1580 }
1581 _ => panic!("Expected Other error"),
1582 }
1583 }
1584
1585 #[test]
1586 fn test_categorize_stellar_error_with_context_no_context() {
1587 let err = StellarClientError::InvalidResponse;
1589 let result = categorize_stellar_error_with_context(err, None);
1590 match result {
1591 ProviderError::Other(msg) => {
1592 assert!(!msg.contains(":")); assert!(msg.contains("Invalid response"));
1594 }
1595 _ => panic!("Expected Other error"),
1596 }
1597 }
1598
1599 #[test]
1600 fn test_initialize_provider_invalid_url() {
1601 let _env_guard = setup_test_env();
1602 let provider = StellarProvider::new(
1603 vec![RpcConfig::new("http://localhost:8000".to_string())],
1604 30,
1605 )
1606 .unwrap();
1607
1608 let result = provider.initialize_provider("invalid-url");
1610 assert!(result.is_err());
1611 match result.unwrap_err() {
1612 ProviderError::NetworkConfiguration(msg) => {
1613 assert!(msg.contains("Failed to create Stellar RPC client"));
1614 }
1615 _ => panic!("Expected NetworkConfiguration error"),
1616 }
1617 }
1618
1619 #[test]
1620 fn test_initialize_raw_provider_timeout_config() {
1621 let _env_guard = setup_test_env();
1622 let provider = StellarProvider::new(
1623 vec![RpcConfig::new("http://localhost:8000".to_string())],
1624 30,
1625 )
1626 .unwrap();
1627
1628 let result = provider.initialize_raw_provider("http://localhost:8000");
1630 assert!(result.is_ok());
1631
1632 let result = provider.initialize_raw_provider("not-a-url");
1635 assert!(result.is_ok() || result.is_err());
1638 }
1639
1640 #[tokio::test]
1641 async fn test_raw_request_dyn_success() {
1642 let _env_guard = setup_test_env();
1643
1644 let provider =
1646 StellarProvider::new(vec![RpcConfig::new("http://127.0.0.1:9999".to_string())], 1)
1647 .unwrap();
1648
1649 let params = serde_json::json!({"test": "value"});
1650 let result = provider
1651 .raw_request_dyn("test_method", params, Some(JsonRpcId::Number(1)))
1652 .await;
1653
1654 assert!(result.is_err());
1656 let err = result.unwrap_err();
1657 assert!(matches!(
1659 err,
1660 ProviderError::Other(_)
1661 | ProviderError::Timeout
1662 | ProviderError::NetworkConfiguration(_)
1663 ));
1664 }
1665
1666 #[tokio::test]
1667 async fn test_raw_request_dyn_with_auto_generated_id() {
1668 let _env_guard = setup_test_env();
1669
1670 let provider =
1671 StellarProvider::new(vec![RpcConfig::new("http://127.0.0.1:9999".to_string())], 1)
1672 .unwrap();
1673
1674 let params = serde_json::json!({"test": "value"});
1675 let result = provider.raw_request_dyn("test_method", params, None).await;
1676
1677 assert!(result.is_err());
1679 }
1680
1681 #[tokio::test]
1682 async fn test_retry_raw_request_connection_failure() {
1683 let _env_guard = setup_test_env();
1684
1685 let provider =
1686 StellarProvider::new(vec![RpcConfig::new("http://127.0.0.1:9999".to_string())], 1)
1687 .unwrap();
1688
1689 let request = serde_json::json!({
1690 "jsonrpc": "2.0",
1691 "id": 1,
1692 "method": "test",
1693 "params": {}
1694 });
1695
1696 let result = provider.retry_raw_request("test_operation", request).await;
1697
1698 assert!(result.is_err());
1700 let err = result.unwrap_err();
1701 assert!(matches!(
1703 err,
1704 ProviderError::Other(_) | ProviderError::Timeout
1705 ));
1706 }
1707
1708 #[tokio::test]
1709 async fn test_raw_request_dyn_json_rpc_error_response() {
1710 let _env_guard = setup_test_env();
1711
1712 let provider =
1715 StellarProvider::new(vec![RpcConfig::new("http://127.0.0.1:9999".to_string())], 1)
1716 .unwrap();
1717
1718 let params = serde_json::json!({"test": "value"});
1719 let result = provider
1720 .raw_request_dyn(
1721 "test_method",
1722 params,
1723 Some(JsonRpcId::String("test-id".to_string())),
1724 )
1725 .await;
1726
1727 assert!(result.is_err());
1729 }
1730
1731 #[test]
1732 fn test_provider_creation_edge_cases() {
1733 let _env_guard = setup_test_env();
1734
1735 let result = StellarProvider::new(vec![], 30);
1737 assert!(result.is_err());
1738 match result.unwrap_err() {
1739 ProviderError::NetworkConfiguration(msg) => {
1740 assert!(msg.contains("No RPC configurations provided"));
1741 }
1742 _ => panic!("Expected NetworkConfiguration error"),
1743 }
1744
1745 let mut config1 = RpcConfig::new("http://localhost:8000".to_string());
1747 config1.weight = 0;
1748 let mut config2 = RpcConfig::new("http://localhost:8001".to_string());
1749 config2.weight = 0;
1750 let configs = vec![config1, config2];
1751 let result = StellarProvider::new(configs, 30);
1752 assert!(result.is_err());
1753 match result.unwrap_err() {
1754 ProviderError::NetworkConfiguration(msg) => {
1755 assert!(msg.contains("No active RPC configurations"));
1756 }
1757 _ => panic!("Expected NetworkConfiguration error"),
1758 }
1759 }
1760
1761 #[tokio::test]
1762 async fn test_get_events_empty_request() {
1763 let _env_guard = setup_test_env();
1764
1765 let mut mock = MockStellarProviderTrait::new();
1766 mock.expect_get_events()
1767 .withf(|req| req.contract_ids.is_empty() && req.topics.is_empty())
1768 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
1769
1770 let req = GetEventsRequest {
1771 start: EventStart::Ledger(1),
1772 event_type: Some(EventType::Contract),
1773 contract_ids: vec![],
1774 topics: vec![],
1775 limit: Some(10),
1776 };
1777
1778 let result = mock.get_events(req).await;
1779 assert!(result.is_ok());
1780 }
1781
1782 #[tokio::test]
1783 async fn test_get_ledger_entries_empty_keys() {
1784 let _env_guard = setup_test_env();
1785
1786 let mut mock = MockStellarProviderTrait::new();
1787 mock.expect_get_ledger_entries()
1788 .withf(|keys| keys.is_empty())
1789 .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
1790
1791 let result = mock.get_ledger_entries(&[]).await;
1792 assert!(result.is_ok());
1793 }
1794
1795 #[tokio::test]
1796 async fn test_send_transaction_polling_success() {
1797 let _env_guard = setup_test_env();
1798
1799 let mut mock = MockStellarProviderTrait::new();
1800 mock.expect_send_transaction_polling()
1801 .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
1802
1803 let envelope = dummy_transaction_envelope();
1804 let result = mock.send_transaction_polling(&envelope).await;
1805 assert!(result.is_ok());
1806 }
1807
1808 #[tokio::test]
1809 async fn test_get_transactions_with_pagination() {
1810 let _env_guard = setup_test_env();
1811
1812 let mut mock = MockStellarProviderTrait::new();
1813 mock.expect_get_transactions()
1814 .returning(|_| async { Ok(dummy_get_transactions_response()) }.boxed());
1815
1816 let req = GetTransactionsRequest {
1817 start_ledger: Some(1000),
1818 pagination: None, };
1820
1821 let result = mock.get_transactions(req).await;
1822 assert!(result.is_ok());
1823 }
1824}