1use async_trait::async_trait;
8use eyre::{eyre, Result};
9use soroban_rs::stellar_rpc_client::Client;
10use soroban_rs::stellar_rpc_client::{
11 EventStart, EventType, GetEventsResponse, GetLatestLedgerResponse, GetLedgerEntriesResponse,
12 GetNetworkResponse, GetTransactionResponse, GetTransactionsRequest, GetTransactionsResponse,
13 SimulateTransactionResponse,
14};
15use soroban_rs::xdr::{AccountEntry, Hash, LedgerKey, TransactionEnvelope};
16#[cfg(test)]
17use soroban_rs::xdr::{AccountId, LedgerKeyAccount, PublicKey, Uint256};
18use soroban_rs::SorobanTransactionResponse;
19
20#[cfg(test)]
21use mockall::automock;
22
23use crate::models::RpcConfig;
24use crate::services::provider::ProviderError;
25
26#[derive(Debug, Clone)]
27pub struct GetEventsRequest {
28 pub start: EventStart,
29 pub event_type: Option<EventType>,
30 pub contract_ids: Vec<String>,
31 pub topics: Vec<String>,
32 pub limit: Option<usize>,
33}
34
35#[derive(Clone, Debug)]
36pub struct StellarProvider {
37 client: Client,
38 rpc_url: String,
39}
40
41#[async_trait]
42#[cfg_attr(test, automock)]
43#[allow(dead_code)]
44pub trait StellarProviderTrait: Send + Sync {
45 async fn get_account(&self, account_id: &str) -> Result<AccountEntry>;
46 async fn simulate_transaction_envelope(
47 &self,
48 tx_envelope: &TransactionEnvelope,
49 ) -> Result<SimulateTransactionResponse>;
50 async fn send_transaction_polling(
51 &self,
52 tx_envelope: &TransactionEnvelope,
53 ) -> Result<SorobanTransactionResponse>;
54 async fn get_network(&self) -> Result<GetNetworkResponse>;
55 async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse>;
56 async fn send_transaction(&self, tx_envelope: &TransactionEnvelope) -> Result<Hash>;
57 async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse>;
58 async fn get_transactions(
59 &self,
60 request: GetTransactionsRequest,
61 ) -> Result<GetTransactionsResponse>;
62 async fn get_ledger_entries(&self, keys: &[LedgerKey]) -> Result<GetLedgerEntriesResponse>;
63 async fn get_events(&self, request: GetEventsRequest) -> Result<GetEventsResponse>;
64 fn rpc_url(&self) -> &str;
65}
66
67impl StellarProvider {
68 pub fn new(mut rpc_configs: Vec<RpcConfig>, _timeout: u64) -> Result<Self, ProviderError> {
69 if rpc_configs.is_empty() {
70 return Err(ProviderError::NetworkConfiguration(
71 "No RPC configurations provided for StellarProvider".to_string(),
72 ));
73 }
74
75 RpcConfig::validate_list(&rpc_configs)
76 .map_err(|e| ProviderError::NetworkConfiguration(e.to_string()))?;
77
78 rpc_configs.retain(|config| config.get_weight() > 0);
79
80 if rpc_configs.is_empty() {
81 return Err(ProviderError::NetworkConfiguration(
82 "No active RPC configurations provided (all weights are 0 or list was empty after filtering)".to_string(),
83 ));
84 }
85
86 rpc_configs.sort_by_key(|config| std::cmp::Reverse(config.get_weight()));
87
88 let selected_config = &rpc_configs[0];
89 let url = &selected_config.url;
90
91 let client = Client::new(url).map_err(|e| {
92 ProviderError::NetworkConfiguration(format!(
93 "Failed to create Stellar RPC client: {} - URL: '{}'",
94 e, url
95 ))
96 })?;
97 Ok(Self {
98 client,
99 rpc_url: url.clone(),
100 })
101 }
102
103 pub fn rpc_url(&self) -> &str {
104 &self.rpc_url
105 }
106}
107
108impl AsRef<StellarProvider> for StellarProvider {
109 fn as_ref(&self) -> &StellarProvider {
110 self
111 }
112}
113
114#[async_trait]
115impl StellarProviderTrait for StellarProvider {
116 async fn get_account(&self, account_id: &str) -> Result<AccountEntry> {
117 self.client
118 .get_account(account_id)
119 .await
120 .map_err(|e| eyre!("Failed to get account: {}", e))
121 }
122
123 async fn simulate_transaction_envelope(
124 &self,
125 tx_envelope: &TransactionEnvelope,
126 ) -> Result<SimulateTransactionResponse> {
127 self.client
128 .simulate_transaction_envelope(tx_envelope)
129 .await
130 .map_err(|e| eyre!("Failed to simulate transaction: {}", e))
131 }
132
133 async fn send_transaction_polling(
134 &self,
135 tx_envelope: &TransactionEnvelope,
136 ) -> Result<SorobanTransactionResponse> {
137 self.client
138 .send_transaction_polling(tx_envelope)
139 .await
140 .map(SorobanTransactionResponse::from)
141 .map_err(|e| eyre!("Failed to send transaction (polling): {}", e))
142 }
143
144 async fn get_network(&self) -> Result<GetNetworkResponse> {
145 self.client
146 .get_network()
147 .await
148 .map_err(|e| eyre!("Failed to get network: {}", e))
149 }
150
151 async fn get_latest_ledger(&self) -> Result<GetLatestLedgerResponse> {
152 self.client
153 .get_latest_ledger()
154 .await
155 .map_err(|e| eyre!("Failed to get latest ledger: {}", e))
156 }
157
158 async fn send_transaction(&self, tx_envelope: &TransactionEnvelope) -> Result<Hash> {
159 self.client
160 .send_transaction(tx_envelope)
161 .await
162 .map_err(|e| eyre!("Failed to send transaction: {}", e))
163 }
164
165 async fn get_transaction(&self, tx_id: &Hash) -> Result<GetTransactionResponse> {
166 self.client
167 .get_transaction(tx_id)
168 .await
169 .map_err(|e| eyre!("Failed to get transaction: {}", e))
170 }
171
172 async fn get_transactions(
173 &self,
174 request: GetTransactionsRequest,
175 ) -> Result<GetTransactionsResponse> {
176 self.client
177 .get_transactions(request)
178 .await
179 .map_err(|e| eyre!("Failed to get transactions: {}", e))
180 }
181
182 async fn get_ledger_entries(&self, keys: &[LedgerKey]) -> Result<GetLedgerEntriesResponse> {
183 self.client
184 .get_ledger_entries(keys)
185 .await
186 .map_err(|e| eyre!("Failed to get ledger entries: {}", e))
187 }
188
189 async fn get_events(&self, request: GetEventsRequest) -> Result<GetEventsResponse> {
190 self.client
191 .get_events(
192 request.start,
193 request.event_type,
194 &request.contract_ids,
195 &request.topics,
196 request.limit,
197 )
198 .await
199 .map_err(|e| eyre!("Failed to get events: {}", e))
200 }
201
202 fn rpc_url(&self) -> &str {
203 &self.rpc_url
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::services::provider::stellar::{
211 GetEventsRequest, StellarProvider, StellarProviderTrait,
212 };
213 use eyre::eyre;
214 use futures::FutureExt;
215 use mockall::predicate as p;
216 use soroban_rs::stellar_rpc_client::{
217 EventStart, GetEventsResponse, GetLatestLedgerResponse, GetLedgerEntriesResponse,
218 GetNetworkResponse, GetTransactionResponse, GetTransactionsRequest,
219 GetTransactionsResponse, SimulateTransactionResponse,
220 };
221 use soroban_rs::xdr::{
222 AccountEntryExt, Hash, LedgerKey, OperationResult, String32, Thresholds,
223 TransactionEnvelope, TransactionResult, TransactionResultExt, TransactionResultResult,
224 VecM,
225 };
226 use soroban_rs::{create_mock_set_options_tx_envelope, SorobanTransactionResponse};
227 use std::str::FromStr;
228
229 fn dummy_hash() -> Hash {
230 Hash([0u8; 32])
231 }
232
233 fn dummy_get_network_response() -> GetNetworkResponse {
234 GetNetworkResponse {
235 friendbot_url: Some("https://friendbot.testnet.stellar.org/".into()),
236 passphrase: "Test SDF Network ; September 2015".into(),
237 protocol_version: 20,
238 }
239 }
240
241 fn dummy_get_latest_ledger_response() -> GetLatestLedgerResponse {
242 GetLatestLedgerResponse {
243 id: "c73c5eac58a441d4eb733c35253ae85f783e018f7be5ef974258fed067aabb36".into(),
244 protocol_version: 20,
245 sequence: 2_539_605,
246 }
247 }
248
249 fn dummy_simulate() -> SimulateTransactionResponse {
250 SimulateTransactionResponse {
251 min_resource_fee: 100,
252 transaction_data: "test".to_string(),
253 ..Default::default()
254 }
255 }
256
257 fn create_success_tx_result() -> TransactionResult {
258 let empty_vec: Vec<OperationResult> = Vec::new();
260 let op_results = empty_vec.try_into().unwrap_or_default();
261
262 TransactionResult {
263 fee_charged: 100,
264 result: TransactionResultResult::TxSuccess(op_results),
265 ext: TransactionResultExt::V0,
266 }
267 }
268
269 fn dummy_get_transaction_response() -> GetTransactionResponse {
270 GetTransactionResponse {
271 status: "SUCCESS".to_string(),
272 envelope: None,
273 result: Some(create_success_tx_result()),
274 result_meta: None,
275 }
276 }
277
278 fn dummy_soroban_tx() -> SorobanTransactionResponse {
279 SorobanTransactionResponse {
280 response: dummy_get_transaction_response(),
281 }
282 }
283
284 fn dummy_get_transactions_response() -> GetTransactionsResponse {
285 GetTransactionsResponse {
286 transactions: vec![],
287 latest_ledger: 0,
288 latest_ledger_close_time: 0,
289 oldest_ledger: 0,
290 oldest_ledger_close_time: 0,
291 cursor: 0,
292 }
293 }
294
295 fn dummy_get_ledger_entries_response() -> GetLedgerEntriesResponse {
296 GetLedgerEntriesResponse {
297 entries: None,
298 latest_ledger: 0,
299 }
300 }
301
302 fn dummy_get_events_response() -> GetEventsResponse {
303 GetEventsResponse {
304 events: vec![],
305 latest_ledger: 0,
306 }
307 }
308
309 fn dummy_transaction_envelope() -> TransactionEnvelope {
310 create_mock_set_options_tx_envelope()
311 }
312
313 fn dummy_ledger_key() -> LedgerKey {
314 LedgerKey::Account(LedgerKeyAccount {
315 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
316 })
317 }
318
319 pub fn mock_account_entry(account_id: &str) -> AccountEntry {
320 AccountEntry {
321 account_id: AccountId(PublicKey::from_str(account_id).unwrap()),
322 balance: 0,
323 ext: AccountEntryExt::V0,
324 flags: 0,
325 home_domain: String32::default(),
326 inflation_dest: None,
327 seq_num: 0.into(),
328 num_sub_entries: 0,
329 signers: VecM::default(),
330 thresholds: Thresholds([0, 0, 0, 0]),
331 }
332 }
333
334 fn dummy_account_entry() -> AccountEntry {
335 mock_account_entry("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
336 }
337
338 #[test]
343 fn test_new_provider() {
344 let provider =
345 StellarProvider::new(vec![RpcConfig::new("http://localhost:8000".to_string())], 0);
346 assert!(provider.is_ok());
347
348 let provider_err = StellarProvider::new(vec![], 0);
349 assert!(provider_err.is_err());
350 match provider_err.unwrap_err() {
351 ProviderError::NetworkConfiguration(msg) => {
352 assert!(msg.contains("No RPC configurations provided"));
353 }
354 _ => panic!("Unexpected error type"),
355 }
356 }
357
358 #[test]
359 fn test_new_provider_selects_highest_weight() {
360 let configs = vec![
361 RpcConfig::with_weight("http://rpc1.example.com".to_string(), 10).unwrap(),
362 RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), RpcConfig::with_weight("http://rpc3.example.com".to_string(), 50).unwrap(),
364 ];
365 let provider = StellarProvider::new(configs, 0);
366 assert!(provider.is_ok());
367 }
371
372 #[test]
373 fn test_new_provider_ignores_weight_zero() {
374 let configs = vec![
375 RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(), RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), ];
378 let provider = StellarProvider::new(configs, 0);
379 assert!(provider.is_ok());
380
381 let configs_only_zero =
382 vec![RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap()];
383 let provider_err = StellarProvider::new(configs_only_zero, 0);
384 assert!(provider_err.is_err());
385 match provider_err.unwrap_err() {
386 ProviderError::NetworkConfiguration(msg) => {
387 assert!(msg.contains("No active RPC configurations provided"));
388 }
389 _ => panic!("Unexpected error type"),
390 }
391 }
392
393 #[test]
394 fn test_new_provider_invalid_url_scheme() {
395 let configs = vec![RpcConfig::new("ftp://invalid.example.com".to_string())];
396 let provider_err = StellarProvider::new(configs, 0);
397 assert!(provider_err.is_err());
398 match provider_err.unwrap_err() {
399 ProviderError::NetworkConfiguration(msg) => {
400 assert!(msg.contains("Invalid URL scheme"));
401 }
402 _ => panic!("Unexpected error type"),
403 }
404 }
405
406 #[test]
407 fn test_new_provider_all_zero_weight_configs() {
408 let configs = vec![
409 RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(),
410 RpcConfig::with_weight("http://rpc2.example.com".to_string(), 0).unwrap(),
411 ];
412 let provider_err = StellarProvider::new(configs, 0);
413 assert!(provider_err.is_err());
414 match provider_err.unwrap_err() {
415 ProviderError::NetworkConfiguration(msg) => {
416 assert!(msg.contains("No active RPC configurations provided"));
417 }
418 _ => panic!("Unexpected error type"),
419 }
420 }
421
422 #[tokio::test]
423 async fn test_mock_basic_methods() {
424 let mut mock = MockStellarProviderTrait::new();
425
426 mock.expect_get_network()
427 .times(1)
428 .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
429
430 mock.expect_get_latest_ledger()
431 .times(1)
432 .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
433
434 assert!(mock.get_network().await.is_ok());
435 assert!(mock.get_latest_ledger().await.is_ok());
436 }
437
438 #[tokio::test]
439 async fn test_mock_transaction_flow() {
440 let mut mock = MockStellarProviderTrait::new();
441
442 let envelope: TransactionEnvelope = dummy_transaction_envelope();
443 let hash = dummy_hash();
444
445 mock.expect_simulate_transaction_envelope()
446 .withf(|_| true)
447 .times(1)
448 .returning(|_| async { Ok(dummy_simulate()) }.boxed());
449
450 mock.expect_send_transaction()
451 .withf(|_| true)
452 .times(1)
453 .returning(|_| async { Ok(dummy_hash()) }.boxed());
454
455 mock.expect_send_transaction_polling()
456 .withf(|_| true)
457 .times(1)
458 .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
459
460 mock.expect_get_transaction()
461 .withf(|_| true)
462 .times(1)
463 .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
464
465 mock.simulate_transaction_envelope(&envelope).await.unwrap();
466 mock.send_transaction(&envelope).await.unwrap();
467 mock.send_transaction_polling(&envelope).await.unwrap();
468 mock.get_transaction(&hash).await.unwrap();
469 }
470
471 #[tokio::test]
472 async fn test_mock_events_and_entries() {
473 let mut mock = MockStellarProviderTrait::new();
474
475 mock.expect_get_events()
476 .times(1)
477 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
478
479 mock.expect_get_ledger_entries()
480 .times(1)
481 .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
482
483 let events_request = GetEventsRequest {
484 start: EventStart::Ledger(1),
485 event_type: None,
486 contract_ids: vec![],
487 topics: vec![],
488 limit: Some(10),
489 };
490
491 let dummy_key: LedgerKey = dummy_ledger_key();
492 mock.get_events(events_request).await.unwrap();
493 mock.get_ledger_entries(&[dummy_key]).await.unwrap();
494 }
495
496 #[tokio::test]
497 async fn test_mock_all_methods_ok() {
498 let mut mock = MockStellarProviderTrait::new();
499
500 mock.expect_get_account()
501 .with(p::eq("GTESTACCOUNTID"))
502 .times(1)
503 .returning(|_| async { Ok(dummy_account_entry()) }.boxed());
504
505 mock.expect_simulate_transaction_envelope()
506 .times(1)
507 .returning(|_| async { Ok(dummy_simulate()) }.boxed());
508
509 mock.expect_send_transaction_polling()
510 .times(1)
511 .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
512
513 mock.expect_get_network()
514 .times(1)
515 .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
516
517 mock.expect_get_latest_ledger()
518 .times(1)
519 .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
520
521 mock.expect_send_transaction()
522 .times(1)
523 .returning(|_| async { Ok(dummy_hash()) }.boxed());
524
525 mock.expect_get_transaction()
526 .times(1)
527 .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
528
529 mock.expect_get_transactions()
530 .times(1)
531 .returning(|_| async { Ok(dummy_get_transactions_response()) }.boxed());
532
533 mock.expect_get_ledger_entries()
534 .times(1)
535 .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
536
537 mock.expect_get_events()
538 .times(1)
539 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
540
541 let _ = mock.get_account("GTESTACCOUNTID").await.unwrap();
542 let env: TransactionEnvelope = dummy_transaction_envelope();
543 mock.simulate_transaction_envelope(&env).await.unwrap();
544 mock.send_transaction_polling(&env).await.unwrap();
545 mock.get_network().await.unwrap();
546 mock.get_latest_ledger().await.unwrap();
547 mock.send_transaction(&env).await.unwrap();
548
549 let h = dummy_hash();
550 mock.get_transaction(&h).await.unwrap();
551
552 let req: GetTransactionsRequest = GetTransactionsRequest {
553 start_ledger: None,
554 pagination: None,
555 };
556 mock.get_transactions(req).await.unwrap();
557
558 let key: LedgerKey = dummy_ledger_key();
559 mock.get_ledger_entries(&[key]).await.unwrap();
560
561 let ev_req = GetEventsRequest {
562 start: EventStart::Ledger(0),
563 event_type: None,
564 contract_ids: vec![],
565 topics: vec![],
566 limit: None,
567 };
568 mock.get_events(ev_req).await.unwrap();
569 }
570
571 #[tokio::test]
572 async fn test_error_propagation() {
573 let mut mock = MockStellarProviderTrait::new();
574
575 mock.expect_get_account()
576 .returning(|_| async { Err(eyre!("boom")) }.boxed());
577
578 let res = mock.get_account("BAD").await;
579 assert!(res.is_err());
580 assert!(res.unwrap_err().to_string().contains("boom"));
581 }
582
583 #[tokio::test]
584 async fn test_get_events_edge_cases() {
585 let mut mock = MockStellarProviderTrait::new();
586
587 mock.expect_get_events()
588 .withf(|req| {
589 req.contract_ids.is_empty() && req.topics.is_empty() && req.limit.is_none()
590 })
591 .times(1)
592 .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
593
594 let ev_req = GetEventsRequest {
595 start: EventStart::Ledger(0),
596 event_type: None,
597 contract_ids: vec![],
598 topics: vec![],
599 limit: None,
600 };
601
602 mock.get_events(ev_req).await.unwrap();
603 }
604
605 #[test]
606 fn test_provider_send_sync_bounds() {
607 fn assert_send_sync<T: Send + Sync>() {}
608 assert_send_sync::<StellarProvider>();
609 }
610
611 #[cfg(test)]
612 mod concrete_tests {
613 use super::*;
614
615 const NON_EXISTENT_URL: &str = "http://127.0.0.1:9999";
616
617 fn setup_provider() -> StellarProvider {
618 StellarProvider::new(vec![RpcConfig::new(NON_EXISTENT_URL.to_string())], 0)
619 .expect("Provider creation should succeed even with bad URL")
620 }
621
622 #[tokio::test]
623 async fn test_concrete_get_account_error() {
624 let provider = setup_provider();
625 let result = provider.get_account("SOME_ACCOUNT_ID").await;
626 assert!(result.is_err());
627 assert!(result
628 .unwrap_err()
629 .to_string()
630 .contains("Failed to get account"));
631 }
632
633 #[tokio::test]
634 async fn test_concrete_simulate_transaction_envelope_error() {
635 let provider = setup_provider();
636 let envelope: TransactionEnvelope = dummy_transaction_envelope();
637 let result = provider.simulate_transaction_envelope(&envelope).await;
638 assert!(result.is_err());
639 assert!(result
640 .unwrap_err()
641 .to_string()
642 .contains("Failed to simulate transaction"));
643 }
644
645 #[tokio::test]
646 async fn test_concrete_send_transaction_polling_error() {
647 let provider = setup_provider();
648 let envelope: TransactionEnvelope = dummy_transaction_envelope();
649 let result = provider.send_transaction_polling(&envelope).await;
650 assert!(result.is_err());
651 assert!(result
652 .unwrap_err()
653 .to_string()
654 .contains("Failed to send transaction (polling)"));
655 }
656
657 #[tokio::test]
658 async fn test_concrete_get_network_error() {
659 let provider = setup_provider();
660 let result = provider.get_network().await;
661 assert!(result.is_err());
662 assert!(result
663 .unwrap_err()
664 .to_string()
665 .contains("Failed to get network"));
666 }
667
668 #[tokio::test]
669 async fn test_concrete_get_latest_ledger_error() {
670 let provider = setup_provider();
671 let result = provider.get_latest_ledger().await;
672 assert!(result.is_err());
673 assert!(result
674 .unwrap_err()
675 .to_string()
676 .contains("Failed to get latest ledger"));
677 }
678
679 #[tokio::test]
680 async fn test_concrete_send_transaction_error() {
681 let provider = setup_provider();
682 let envelope: TransactionEnvelope = dummy_transaction_envelope();
683 let result = provider.send_transaction(&envelope).await;
684 assert!(result.is_err());
685 assert!(result
686 .unwrap_err()
687 .to_string()
688 .contains("Failed to send transaction"));
689 }
690
691 #[tokio::test]
692 async fn test_concrete_get_transaction_error() {
693 let provider = setup_provider();
694 let hash: Hash = dummy_hash();
695 let result = provider.get_transaction(&hash).await;
696 assert!(result.is_err());
697 assert!(result
698 .unwrap_err()
699 .to_string()
700 .contains("Failed to get transaction"));
701 }
702
703 #[tokio::test]
704 async fn test_concrete_get_transactions_error() {
705 let provider = setup_provider();
706 let req = GetTransactionsRequest {
707 start_ledger: None,
708 pagination: None,
709 };
710 let result = provider.get_transactions(req).await;
711 assert!(result.is_err());
712 assert!(result
713 .unwrap_err()
714 .to_string()
715 .contains("Failed to get transactions"));
716 }
717
718 #[tokio::test]
719 async fn test_concrete_get_ledger_entries_error() {
720 let provider = setup_provider();
721 let key: LedgerKey = dummy_ledger_key();
722 let result = provider.get_ledger_entries(&[key]).await;
723 assert!(result.is_err());
724 assert!(result
725 .unwrap_err()
726 .to_string()
727 .contains("Failed to get ledger entries"));
728 }
729
730 #[tokio::test]
731 async fn test_concrete_get_events_error() {
732 let provider = setup_provider();
733 let req = GetEventsRequest {
734 start: EventStart::Ledger(1),
735 event_type: None,
736 contract_ids: vec![],
737 topics: vec![],
738 limit: None,
739 };
740 let result = provider.get_events(req).await;
741 assert!(result.is_err());
742 assert!(result
743 .unwrap_err()
744 .to_string()
745 .contains("Failed to get events"));
746 }
747 }
748}