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