openzeppelin_relayer/services/provider/stellar/
mod.rs

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