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