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, None)
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, GetTransactionEvents, 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            events: GetTransactionEvents {
276                contract_events: vec![],
277                diagnostic_events: vec![],
278                transaction_events: vec![],
279            },
280            ledger: None,
281        }
282    }
283
284    fn dummy_soroban_tx() -> SorobanTransactionResponse {
285        SorobanTransactionResponse {
286            response: dummy_get_transaction_response(),
287        }
288    }
289
290    fn dummy_get_transactions_response() -> GetTransactionsResponse {
291        GetTransactionsResponse {
292            transactions: vec![],
293            latest_ledger: 0,
294            latest_ledger_close_time: 0,
295            oldest_ledger: 0,
296            oldest_ledger_close_time: 0,
297            cursor: 0,
298        }
299    }
300
301    fn dummy_get_ledger_entries_response() -> GetLedgerEntriesResponse {
302        GetLedgerEntriesResponse {
303            entries: None,
304            latest_ledger: 0,
305        }
306    }
307
308    fn dummy_get_events_response() -> GetEventsResponse {
309        GetEventsResponse {
310            events: vec![],
311            latest_ledger: 0,
312            latest_ledger_close_time: "0".to_string(),
313            oldest_ledger: 0,
314            oldest_ledger_close_time: "0".to_string(),
315            cursor: "0".to_string(),
316        }
317    }
318
319    fn dummy_transaction_envelope() -> TransactionEnvelope {
320        create_mock_set_options_tx_envelope()
321    }
322
323    fn dummy_ledger_key() -> LedgerKey {
324        LedgerKey::Account(LedgerKeyAccount {
325            account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
326        })
327    }
328
329    pub fn mock_account_entry(account_id: &str) -> AccountEntry {
330        AccountEntry {
331            account_id: AccountId(PublicKey::from_str(account_id).unwrap()),
332            balance: 0,
333            ext: AccountEntryExt::V0,
334            flags: 0,
335            home_domain: String32::default(),
336            inflation_dest: None,
337            seq_num: 0.into(),
338            num_sub_entries: 0,
339            signers: VecM::default(),
340            thresholds: Thresholds([0, 0, 0, 0]),
341        }
342    }
343
344    fn dummy_account_entry() -> AccountEntry {
345        mock_account_entry("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF")
346    }
347
348    // ---------------------------------------------------------------------
349    // Tests
350    // ---------------------------------------------------------------------
351
352    #[test]
353    fn test_new_provider() {
354        let provider =
355            StellarProvider::new(vec![RpcConfig::new("http://localhost:8000".to_string())], 0);
356        assert!(provider.is_ok());
357
358        let provider_err = StellarProvider::new(vec![], 0);
359        assert!(provider_err.is_err());
360        match provider_err.unwrap_err() {
361            ProviderError::NetworkConfiguration(msg) => {
362                assert!(msg.contains("No RPC configurations provided"));
363            }
364            _ => panic!("Unexpected error type"),
365        }
366    }
367
368    #[test]
369    fn test_new_provider_selects_highest_weight() {
370        let configs = vec![
371            RpcConfig::with_weight("http://rpc1.example.com".to_string(), 10).unwrap(),
372            RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), // Highest weight
373            RpcConfig::with_weight("http://rpc3.example.com".to_string(), 50).unwrap(),
374        ];
375        let provider = StellarProvider::new(configs, 0);
376        assert!(provider.is_ok());
377        // We can't directly inspect the client's URL easily without more complex mocking or changes.
378        // For now, we trust the sorting logic and that Client::new would fail for a truly bad URL if selection was wrong.
379        // A more robust test would involve a mock client or a way to inspect the chosen URL.
380    }
381
382    #[test]
383    fn test_new_provider_ignores_weight_zero() {
384        let configs = vec![
385            RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(), // Weight 0
386            RpcConfig::with_weight("http://rpc2.example.com".to_string(), 100).unwrap(), // Should be selected
387        ];
388        let provider = StellarProvider::new(configs, 0);
389        assert!(provider.is_ok());
390
391        let configs_only_zero =
392            vec![RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap()];
393        let provider_err = StellarProvider::new(configs_only_zero, 0);
394        assert!(provider_err.is_err());
395        match provider_err.unwrap_err() {
396            ProviderError::NetworkConfiguration(msg) => {
397                assert!(msg.contains("No active RPC configurations provided"));
398            }
399            _ => panic!("Unexpected error type"),
400        }
401    }
402
403    #[test]
404    fn test_new_provider_invalid_url_scheme() {
405        let configs = vec![RpcConfig::new("ftp://invalid.example.com".to_string())];
406        let provider_err = StellarProvider::new(configs, 0);
407        assert!(provider_err.is_err());
408        match provider_err.unwrap_err() {
409            ProviderError::NetworkConfiguration(msg) => {
410                assert!(msg.contains("Invalid URL scheme"));
411            }
412            _ => panic!("Unexpected error type"),
413        }
414    }
415
416    #[test]
417    fn test_new_provider_all_zero_weight_configs() {
418        let configs = vec![
419            RpcConfig::with_weight("http://rpc1.example.com".to_string(), 0).unwrap(),
420            RpcConfig::with_weight("http://rpc2.example.com".to_string(), 0).unwrap(),
421        ];
422        let provider_err = StellarProvider::new(configs, 0);
423        assert!(provider_err.is_err());
424        match provider_err.unwrap_err() {
425            ProviderError::NetworkConfiguration(msg) => {
426                assert!(msg.contains("No active RPC configurations provided"));
427            }
428            _ => panic!("Unexpected error type"),
429        }
430    }
431
432    #[tokio::test]
433    async fn test_mock_basic_methods() {
434        let mut mock = MockStellarProviderTrait::new();
435
436        mock.expect_get_network()
437            .times(1)
438            .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
439
440        mock.expect_get_latest_ledger()
441            .times(1)
442            .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
443
444        assert!(mock.get_network().await.is_ok());
445        assert!(mock.get_latest_ledger().await.is_ok());
446    }
447
448    #[tokio::test]
449    async fn test_mock_transaction_flow() {
450        let mut mock = MockStellarProviderTrait::new();
451
452        let envelope: TransactionEnvelope = dummy_transaction_envelope();
453        let hash = dummy_hash();
454
455        mock.expect_simulate_transaction_envelope()
456            .withf(|_| true)
457            .times(1)
458            .returning(|_| async { Ok(dummy_simulate()) }.boxed());
459
460        mock.expect_send_transaction()
461            .withf(|_| true)
462            .times(1)
463            .returning(|_| async { Ok(dummy_hash()) }.boxed());
464
465        mock.expect_send_transaction_polling()
466            .withf(|_| true)
467            .times(1)
468            .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
469
470        mock.expect_get_transaction()
471            .withf(|_| true)
472            .times(1)
473            .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
474
475        mock.simulate_transaction_envelope(&envelope).await.unwrap();
476        mock.send_transaction(&envelope).await.unwrap();
477        mock.send_transaction_polling(&envelope).await.unwrap();
478        mock.get_transaction(&hash).await.unwrap();
479    }
480
481    #[tokio::test]
482    async fn test_mock_events_and_entries() {
483        let mut mock = MockStellarProviderTrait::new();
484
485        mock.expect_get_events()
486            .times(1)
487            .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
488
489        mock.expect_get_ledger_entries()
490            .times(1)
491            .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
492
493        let events_request = GetEventsRequest {
494            start: EventStart::Ledger(1),
495            event_type: None,
496            contract_ids: vec![],
497            topics: vec![],
498            limit: Some(10),
499        };
500
501        let dummy_key: LedgerKey = dummy_ledger_key();
502        mock.get_events(events_request).await.unwrap();
503        mock.get_ledger_entries(&[dummy_key]).await.unwrap();
504    }
505
506    #[tokio::test]
507    async fn test_mock_all_methods_ok() {
508        let mut mock = MockStellarProviderTrait::new();
509
510        mock.expect_get_account()
511            .with(p::eq("GTESTACCOUNTID"))
512            .times(1)
513            .returning(|_| async { Ok(dummy_account_entry()) }.boxed());
514
515        mock.expect_simulate_transaction_envelope()
516            .times(1)
517            .returning(|_| async { Ok(dummy_simulate()) }.boxed());
518
519        mock.expect_send_transaction_polling()
520            .times(1)
521            .returning(|_| async { Ok(dummy_soroban_tx()) }.boxed());
522
523        mock.expect_get_network()
524            .times(1)
525            .returning(|| async { Ok(dummy_get_network_response()) }.boxed());
526
527        mock.expect_get_latest_ledger()
528            .times(1)
529            .returning(|| async { Ok(dummy_get_latest_ledger_response()) }.boxed());
530
531        mock.expect_send_transaction()
532            .times(1)
533            .returning(|_| async { Ok(dummy_hash()) }.boxed());
534
535        mock.expect_get_transaction()
536            .times(1)
537            .returning(|_| async { Ok(dummy_get_transaction_response()) }.boxed());
538
539        mock.expect_get_transactions()
540            .times(1)
541            .returning(|_| async { Ok(dummy_get_transactions_response()) }.boxed());
542
543        mock.expect_get_ledger_entries()
544            .times(1)
545            .returning(|_| async { Ok(dummy_get_ledger_entries_response()) }.boxed());
546
547        mock.expect_get_events()
548            .times(1)
549            .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
550
551        let _ = mock.get_account("GTESTACCOUNTID").await.unwrap();
552        let env: TransactionEnvelope = dummy_transaction_envelope();
553        mock.simulate_transaction_envelope(&env).await.unwrap();
554        mock.send_transaction_polling(&env).await.unwrap();
555        mock.get_network().await.unwrap();
556        mock.get_latest_ledger().await.unwrap();
557        mock.send_transaction(&env).await.unwrap();
558
559        let h = dummy_hash();
560        mock.get_transaction(&h).await.unwrap();
561
562        let req: GetTransactionsRequest = GetTransactionsRequest {
563            start_ledger: None,
564            pagination: None,
565        };
566        mock.get_transactions(req).await.unwrap();
567
568        let key: LedgerKey = dummy_ledger_key();
569        mock.get_ledger_entries(&[key]).await.unwrap();
570
571        let ev_req = GetEventsRequest {
572            start: EventStart::Ledger(0),
573            event_type: None,
574            contract_ids: vec![],
575            topics: vec![],
576            limit: None,
577        };
578        mock.get_events(ev_req).await.unwrap();
579    }
580
581    #[tokio::test]
582    async fn test_error_propagation() {
583        let mut mock = MockStellarProviderTrait::new();
584
585        mock.expect_get_account()
586            .returning(|_| async { Err(eyre!("boom")) }.boxed());
587
588        let res = mock.get_account("BAD").await;
589        assert!(res.is_err());
590        assert!(res.unwrap_err().to_string().contains("boom"));
591    }
592
593    #[tokio::test]
594    async fn test_get_events_edge_cases() {
595        let mut mock = MockStellarProviderTrait::new();
596
597        mock.expect_get_events()
598            .withf(|req| {
599                req.contract_ids.is_empty() && req.topics.is_empty() && req.limit.is_none()
600            })
601            .times(1)
602            .returning(|_| async { Ok(dummy_get_events_response()) }.boxed());
603
604        let ev_req = GetEventsRequest {
605            start: EventStart::Ledger(0),
606            event_type: None,
607            contract_ids: vec![],
608            topics: vec![],
609            limit: None,
610        };
611
612        mock.get_events(ev_req).await.unwrap();
613    }
614
615    #[test]
616    fn test_provider_send_sync_bounds() {
617        fn assert_send_sync<T: Send + Sync>() {}
618        assert_send_sync::<StellarProvider>();
619    }
620
621    #[cfg(test)]
622    mod concrete_tests {
623        use super::*;
624
625        const NON_EXISTENT_URL: &str = "http://127.0.0.1:9999";
626
627        fn setup_provider() -> StellarProvider {
628            StellarProvider::new(vec![RpcConfig::new(NON_EXISTENT_URL.to_string())], 0)
629                .expect("Provider creation should succeed even with bad URL")
630        }
631
632        #[tokio::test]
633        async fn test_concrete_get_account_error() {
634            let provider = setup_provider();
635            let result = provider.get_account("SOME_ACCOUNT_ID").await;
636            assert!(result.is_err());
637            assert!(result
638                .unwrap_err()
639                .to_string()
640                .contains("Failed to get account"));
641        }
642
643        #[tokio::test]
644        async fn test_concrete_simulate_transaction_envelope_error() {
645            let provider = setup_provider();
646            let envelope: TransactionEnvelope = dummy_transaction_envelope();
647            let result = provider.simulate_transaction_envelope(&envelope).await;
648            assert!(result.is_err());
649            assert!(result
650                .unwrap_err()
651                .to_string()
652                .contains("Failed to simulate transaction"));
653        }
654
655        #[tokio::test]
656        async fn test_concrete_send_transaction_polling_error() {
657            let provider = setup_provider();
658            let envelope: TransactionEnvelope = dummy_transaction_envelope();
659            let result = provider.send_transaction_polling(&envelope).await;
660            assert!(result.is_err());
661            assert!(result
662                .unwrap_err()
663                .to_string()
664                .contains("Failed to send transaction (polling)"));
665        }
666
667        #[tokio::test]
668        async fn test_concrete_get_network_error() {
669            let provider = setup_provider();
670            let result = provider.get_network().await;
671            assert!(result.is_err());
672            assert!(result
673                .unwrap_err()
674                .to_string()
675                .contains("Failed to get network"));
676        }
677
678        #[tokio::test]
679        async fn test_concrete_get_latest_ledger_error() {
680            let provider = setup_provider();
681            let result = provider.get_latest_ledger().await;
682            assert!(result.is_err());
683            assert!(result
684                .unwrap_err()
685                .to_string()
686                .contains("Failed to get latest ledger"));
687        }
688
689        #[tokio::test]
690        async fn test_concrete_send_transaction_error() {
691            let provider = setup_provider();
692            let envelope: TransactionEnvelope = dummy_transaction_envelope();
693            let result = provider.send_transaction(&envelope).await;
694            assert!(result.is_err());
695            assert!(result
696                .unwrap_err()
697                .to_string()
698                .contains("Failed to send transaction"));
699        }
700
701        #[tokio::test]
702        async fn test_concrete_get_transaction_error() {
703            let provider = setup_provider();
704            let hash: Hash = dummy_hash();
705            let result = provider.get_transaction(&hash).await;
706            assert!(result.is_err());
707            assert!(result
708                .unwrap_err()
709                .to_string()
710                .contains("Failed to get transaction"));
711        }
712
713        #[tokio::test]
714        async fn test_concrete_get_transactions_error() {
715            let provider = setup_provider();
716            let req = GetTransactionsRequest {
717                start_ledger: None,
718                pagination: None,
719            };
720            let result = provider.get_transactions(req).await;
721            assert!(result.is_err());
722            assert!(result
723                .unwrap_err()
724                .to_string()
725                .contains("Failed to get transactions"));
726        }
727
728        #[tokio::test]
729        async fn test_concrete_get_ledger_entries_error() {
730            let provider = setup_provider();
731            let key: LedgerKey = dummy_ledger_key();
732            let result = provider.get_ledger_entries(&[key]).await;
733            assert!(result.is_err());
734            assert!(result
735                .unwrap_err()
736                .to_string()
737                .contains("Failed to get ledger entries"));
738        }
739
740        #[tokio::test]
741        async fn test_concrete_get_events_error() {
742            let provider = setup_provider();
743            let req = GetEventsRequest {
744                start: EventStart::Ledger(1),
745                event_type: None,
746                contract_ids: vec![],
747                topics: vec![],
748                limit: None,
749            };
750            let result = provider.get_events(req).await;
751            assert!(result.is_err());
752            assert!(result
753                .unwrap_err()
754                .to_string()
755                .contains("Failed to get events"));
756        }
757    }
758}