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