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    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        // Create empty operation results
259        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    // ---------------------------------------------------------------------
339    // Tests
340    // ---------------------------------------------------------------------
341
342    #[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(), // Highest weight
363            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        // We can't directly inspect the client's URL easily without more complex mocking or changes.
368        // For now, we trust the sorting logic and that Client::new would fail for a truly bad URL if selection was wrong.
369        // A more robust test would involve a mock client or a way to inspect the chosen URL.
370    }
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(), // Weight 0
376            RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), // Should be selected
377        ];
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}