openzeppelin_relayer/domain/relayer/stellar/
stellar_relayer.rs

1/// This module defines the `StellarRelayer` struct and its associated functionality for
2/// interacting with Stellar networks. The `StellarRelayer` is responsible for managing
3/// transactions, synchronizing sequence numbers, and ensuring the relayer's state is
4/// consistent with the Stellar blockchain.
5///
6/// # Components
7///
8/// - `StellarRelayer`: The main struct that encapsulates the relayer's state and operations for Stellar.
9/// - `RelayerRepoModel`: Represents the relayer's data model.
10/// - `StellarProvider`: Provides blockchain interaction capabilities, such as fetching account details.
11/// - `TransactionCounterService`: Manages the sequence number for transactions to ensure correct ordering.
12/// - `JobProducer`: Produces jobs for processing transactions and sending notifications.
13///
14/// # Error Handling
15///
16/// The module uses the `RelayerError` enum to handle various errors that can occur during
17/// operations, such as provider errors, sequence synchronization failures, and transaction failures.
18///
19/// # Usage
20///
21/// To use the `StellarRelayer`, create an instance using the `new` method, providing the necessary
22/// components. Then, call the appropriate methods to process transactions and manage the relayer's state.
23use crate::{
24    constants::STELLAR_SMALLEST_UNIT_NAME,
25    domain::{
26        stellar::next_sequence_u64, BalanceResponse, SignDataRequest, SignDataResponse,
27        SignTypedDataRequest,
28    },
29    jobs::{JobProducerTrait, TransactionRequest},
30    models::{
31        produce_relayer_disabled_payload, DeletePendingTransactionsResponse, JsonRpcRequest,
32        JsonRpcResponse, NetworkRpcRequest, NetworkRpcResult, NetworkTransactionRequest,
33        NetworkType, RelayerRepoModel, RelayerStatus, RepositoryError, StellarNetwork,
34        StellarRpcResult, TransactionRepoModel, TransactionStatus,
35    },
36    repositories::{
37        InMemoryNetworkRepository, InMemoryRelayerRepository, InMemoryTransactionCounter,
38        InMemoryTransactionRepository, NetworkRepository, RelayerRepository,
39        RelayerRepositoryStorage, Repository, TransactionRepository,
40    },
41    services::{
42        StellarProvider, StellarProviderTrait, TransactionCounterService,
43        TransactionCounterServiceTrait,
44    },
45};
46use async_trait::async_trait;
47use eyre::Result;
48use log::{info, warn};
49use std::sync::Arc;
50
51use crate::domain::relayer::{Relayer, RelayerError};
52
53/// Dependencies container for `StellarRelayer` construction.
54pub struct StellarRelayerDependencies<R, N, T, J, C>
55where
56    R: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync,
57    N: NetworkRepository + Send + Sync,
58    T: Repository<TransactionRepoModel, String> + Send + Sync,
59    J: JobProducerTrait + Send + Sync,
60    C: TransactionCounterServiceTrait + Send + Sync,
61{
62    pub relayer_repository: Arc<R>,
63    pub network_repository: Arc<N>,
64    pub transaction_repository: Arc<T>,
65    pub transaction_counter_service: Arc<C>,
66    pub job_producer: Arc<J>,
67}
68
69impl<R, N, T, J, C> StellarRelayerDependencies<R, N, T, J, C>
70where
71    R: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync,
72    N: NetworkRepository + Send + Sync,
73    T: Repository<TransactionRepoModel, String> + Send + Sync,
74    J: JobProducerTrait + Send + Sync,
75    C: TransactionCounterServiceTrait + Send + Sync,
76{
77    /// Creates a new dependencies container for `StellarRelayer`.
78    ///
79    /// # Arguments
80    ///
81    /// * `relayer_repository` - Repository for managing relayer model persistence
82    /// * `network_repository` - Repository for accessing network configuration data (RPC URLs, chain settings)
83    /// * `transaction_repository` - Repository for storing and retrieving transaction models
84    /// * `transaction_counter_service` - Service for managing sequence numbers to ensure proper transaction ordering
85    /// * `job_producer` - Service for creating background jobs for transaction processing and notifications
86    ///
87    /// # Returns
88    ///
89    /// Returns a new `StellarRelayerDependencies` instance containing all provided dependencies.
90    pub fn new(
91        relayer_repository: Arc<R>,
92        network_repository: Arc<N>,
93        transaction_repository: Arc<T>,
94        transaction_counter_service: Arc<C>,
95        job_producer: Arc<J>,
96    ) -> Self {
97        Self {
98            relayer_repository,
99            network_repository,
100            transaction_repository,
101            transaction_counter_service,
102            job_producer,
103        }
104    }
105}
106
107#[allow(dead_code)]
108pub struct StellarRelayer<P, R, N, T, J, C>
109where
110    P: StellarProviderTrait + Send + Sync,
111    R: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync,
112    N: NetworkRepository + Send + Sync,
113    T: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync,
114    J: JobProducerTrait + Send + Sync,
115    C: TransactionCounterServiceTrait + Send + Sync,
116{
117    relayer: RelayerRepoModel,
118    network: StellarNetwork,
119    provider: P,
120    relayer_repository: Arc<R>,
121    network_repository: Arc<N>,
122    transaction_repository: Arc<T>,
123    transaction_counter_service: Arc<C>,
124    job_producer: Arc<J>,
125}
126
127pub type DefaultStellarRelayer<J> = StellarRelayer<
128    StellarProvider,
129    RelayerRepositoryStorage<InMemoryRelayerRepository>,
130    InMemoryNetworkRepository,
131    InMemoryTransactionRepository,
132    J,
133    TransactionCounterService<InMemoryTransactionCounter>,
134>;
135
136impl<P, R, N, T, J, C> StellarRelayer<P, R, N, T, J, C>
137where
138    P: StellarProviderTrait + Send + Sync,
139    R: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync,
140    N: NetworkRepository + Send + Sync,
141    T: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync,
142    J: JobProducerTrait + Send + Sync,
143    C: TransactionCounterServiceTrait + Send + Sync,
144{
145    /// Creates a new `StellarRelayer` instance.
146    ///
147    /// This constructor initializes a new Stellar relayer with the provided configuration,
148    /// provider, and dependencies. It validates the network configuration and sets up
149    /// all necessary components for transaction processing.
150    ///
151    /// # Arguments
152    ///
153    /// * `relayer` - The relayer model containing configuration like ID, address, network name, and policies
154    /// * `provider` - The Stellar provider implementation for blockchain interactions (account queries, transaction submission)
155    /// * `dependencies` - Container with all required repositories and services (see [`StellarRelayerDependencies`])
156    ///
157    /// # Returns
158    ///
159    /// * `Ok(StellarRelayer)` - Successfully initialized relayer ready for operation
160    /// * `Err(RelayerError)` - If initialization fails due to configuration or validation errors
161    #[allow(clippy::too_many_arguments)]
162    pub async fn new(
163        relayer: RelayerRepoModel,
164        provider: P,
165        dependencies: StellarRelayerDependencies<R, N, T, J, C>,
166    ) -> Result<Self, RelayerError> {
167        let network_repo = dependencies
168            .network_repository
169            .get_by_name(NetworkType::Stellar, &relayer.network)
170            .await
171            .ok()
172            .flatten()
173            .ok_or_else(|| {
174                RelayerError::NetworkConfiguration(format!("Network {} not found", relayer.network))
175            })?;
176
177        let network = StellarNetwork::try_from(network_repo)?;
178
179        Ok(Self {
180            relayer,
181            network,
182            provider,
183            relayer_repository: dependencies.relayer_repository,
184            network_repository: dependencies.network_repository,
185            transaction_repository: dependencies.transaction_repository,
186            transaction_counter_service: dependencies.transaction_counter_service,
187            job_producer: dependencies.job_producer,
188        })
189    }
190
191    async fn sync_sequence(&self) -> Result<(), RelayerError> {
192        info!(
193            "Fetching sequence for relayer: {} ({})",
194            self.relayer.id, self.relayer.address
195        );
196
197        let account_entry = self
198            .provider
199            .get_account(&self.relayer.address)
200            .await
201            .map_err(|e| RelayerError::ProviderError(format!("Failed to fetch account: {}", e)))?;
202
203        let next = next_sequence_u64(account_entry.seq_num.0)?;
204
205        info!(
206            "Setting next sequence {} for relayer {}",
207            next, self.relayer.id
208        );
209        self.transaction_counter_service
210            .set(next)
211            .await
212            .map_err(RelayerError::from)?;
213        Ok(())
214    }
215
216    async fn disable_relayer(&self, reasons: &[String]) -> Result<(), RelayerError> {
217        let reason = reasons.join(", ");
218        warn!("Disabling relayer {} due to: {}", self.relayer.id, reason);
219
220        let updated = self
221            .relayer_repository
222            .disable_relayer(self.relayer.id.clone())
223            .await?;
224
225        if let Some(nid) = &self.relayer.notification_id {
226            self.job_producer
227                .produce_send_notification_job(
228                    produce_relayer_disabled_payload(nid, &updated, &reason),
229                    None,
230                )
231                .await?;
232        }
233        Ok(())
234    }
235}
236
237#[async_trait]
238impl<P, R, N, T, J, C> Relayer for StellarRelayer<P, R, N, T, J, C>
239where
240    P: StellarProviderTrait + Send + Sync,
241    R: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync,
242    N: NetworkRepository + Send + Sync,
243    T: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync,
244    J: JobProducerTrait + Send + Sync,
245    C: TransactionCounterServiceTrait + Send + Sync,
246{
247    async fn process_transaction_request(
248        &self,
249        network_transaction: NetworkTransactionRequest,
250    ) -> Result<TransactionRepoModel, RelayerError> {
251        let network_model = self
252            .network_repository
253            .get_by_name(NetworkType::Stellar, &self.relayer.network)
254            .await?
255            .ok_or_else(|| {
256                RelayerError::NetworkConfiguration(format!(
257                    "Network {} not found",
258                    self.relayer.network
259                ))
260            })?;
261        let transaction =
262            TransactionRepoModel::try_from((&network_transaction, &self.relayer, &network_model))?;
263
264        self.transaction_repository
265            .create(transaction.clone())
266            .await
267            .map_err(|e| RepositoryError::TransactionFailure(e.to_string()))?;
268
269        self.job_producer
270            .produce_transaction_request_job(
271                TransactionRequest::new(transaction.id.clone(), transaction.relayer_id.clone()),
272                None,
273            )
274            .await?;
275
276        Ok(transaction)
277    }
278
279    async fn get_balance(&self) -> Result<BalanceResponse, RelayerError> {
280        let account_entry = self
281            .provider
282            .get_account(&self.relayer.address)
283            .await
284            .map_err(|e| {
285                RelayerError::ProviderError(format!("Failed to fetch account for balance: {}", e))
286            })?;
287
288        Ok(BalanceResponse {
289            balance: account_entry.balance as u128,
290            unit: STELLAR_SMALLEST_UNIT_NAME.to_string(),
291        })
292    }
293
294    async fn get_status(&self) -> Result<RelayerStatus, RelayerError> {
295        let relayer_model = &self.relayer;
296
297        let account_entry = self
298            .provider
299            .get_account(&relayer_model.address)
300            .await
301            .map_err(|e| {
302                RelayerError::ProviderError(format!("Failed to get account details: {}", e))
303            })?;
304
305        let sequence_number_str = account_entry.seq_num.0.to_string();
306
307        let balance_response = self.get_balance().await?;
308
309        let pending_statuses = [TransactionStatus::Pending, TransactionStatus::Submitted];
310        let pending_transactions = self
311            .transaction_repository
312            .find_by_status(&relayer_model.id, &pending_statuses[..])
313            .await
314            .map_err(RelayerError::from)?;
315        let pending_transactions_count = pending_transactions.len() as u64;
316
317        let confirmed_statuses = [TransactionStatus::Confirmed];
318        let confirmed_transactions = self
319            .transaction_repository
320            .find_by_status(&relayer_model.id, &confirmed_statuses[..])
321            .await
322            .map_err(RelayerError::from)?;
323
324        let last_confirmed_transaction_timestamp = confirmed_transactions
325            .iter()
326            .filter_map(|tx| tx.confirmed_at.as_ref())
327            .max()
328            .cloned();
329
330        Ok(RelayerStatus::Stellar {
331            balance: balance_response.balance.to_string(),
332            pending_transactions_count,
333            last_confirmed_transaction_timestamp,
334            system_disabled: relayer_model.system_disabled,
335            paused: relayer_model.paused,
336            sequence_number: sequence_number_str,
337        })
338    }
339
340    async fn delete_pending_transactions(
341        &self,
342    ) -> Result<DeletePendingTransactionsResponse, RelayerError> {
343        println!("Stellar delete_pending_transactions...");
344        Ok(DeletePendingTransactionsResponse {
345            queued_for_cancellation_transaction_ids: vec![],
346            failed_to_queue_transaction_ids: vec![],
347            total_processed: 0,
348        })
349    }
350
351    async fn sign_data(&self, _request: SignDataRequest) -> Result<SignDataResponse, RelayerError> {
352        Err(RelayerError::NotSupported(
353            "Signing data not supported for Stellar".to_string(),
354        ))
355    }
356
357    async fn sign_typed_data(
358        &self,
359        _request: SignTypedDataRequest,
360    ) -> Result<SignDataResponse, RelayerError> {
361        Err(RelayerError::NotSupported(
362            "Signing typed data not supported for Stellar".to_string(),
363        ))
364    }
365
366    async fn rpc(
367        &self,
368        _request: JsonRpcRequest<NetworkRpcRequest>,
369    ) -> Result<JsonRpcResponse<NetworkRpcResult>, RelayerError> {
370        println!("Stellar rpc...");
371        Ok(JsonRpcResponse {
372            id: None,
373            jsonrpc: "2.0".to_string(),
374            result: Some(NetworkRpcResult::Stellar(
375                StellarRpcResult::GenericRpcResult("".to_string()),
376            )),
377            error: None,
378        })
379    }
380
381    async fn validate_min_balance(&self) -> Result<(), RelayerError> {
382        Ok(())
383    }
384
385    async fn initialize_relayer(&self) -> Result<(), RelayerError> {
386        info!("Initializing Stellar relayer: {}", self.relayer.id);
387
388        let seq_res = self.sync_sequence().await.err();
389
390        let mut failures: Vec<String> = Vec::new();
391        if let Some(e) = seq_res {
392            failures.push(format!("Sequence sync failed: {}", e));
393        }
394
395        if !failures.is_empty() {
396            self.disable_relayer(&failures).await?;
397            return Ok(()); // same semantics as EVM
398        }
399
400        info!(
401            "Stellar relayer initialized successfully: {}",
402            self.relayer.id
403        );
404        Ok(())
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use crate::{
412        config::{NetworkConfigCommon, StellarNetworkConfig},
413        constants::STELLAR_SMALLEST_UNIT_NAME,
414        jobs::MockJobProducerTrait,
415        models::{
416            NetworkConfigData, NetworkRepoModel, NetworkType, RelayerNetworkPolicy,
417            RelayerRepoModel, RelayerStellarPolicy,
418        },
419        repositories::{
420            InMemoryNetworkRepository, MockRelayerRepository, MockTransactionRepository,
421        },
422        services::{MockStellarProviderTrait, MockTransactionCounterServiceTrait},
423    };
424    use eyre::eyre;
425    use mockall::predicate::*;
426    use soroban_rs::xdr::{
427        AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32, Thresholds,
428        Uint256, VecM,
429    };
430    use std::future::ready;
431    use std::sync::Arc;
432
433    /// Test context structure to manage test dependencies
434    struct TestCtx {
435        relayer_model: RelayerRepoModel,
436        network_repository: Arc<InMemoryNetworkRepository>,
437    }
438
439    impl Default for TestCtx {
440        fn default() -> Self {
441            let network_repository = Arc::new(InMemoryNetworkRepository::new());
442
443            let relayer_model = RelayerRepoModel {
444                id: "test-relayer-id".to_string(),
445                name: "Test Relayer".to_string(),
446                network: "testnet".to_string(),
447                paused: false,
448                network_type: NetworkType::Stellar,
449                signer_id: "signer-id".to_string(),
450                policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
451                address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
452                notification_id: Some("notification-id".to_string()),
453                system_disabled: false,
454                custom_rpc_urls: None,
455            };
456
457            TestCtx {
458                relayer_model,
459                network_repository,
460            }
461        }
462    }
463
464    impl TestCtx {
465        async fn setup_network(&self) {
466            let test_network = NetworkRepoModel {
467                id: "stellar:testnet".to_string(),
468                name: "testnet".to_string(),
469                network_type: NetworkType::Stellar,
470                config: NetworkConfigData::Stellar(StellarNetworkConfig {
471                    common: NetworkConfigCommon {
472                        network: "testnet".to_string(),
473                        from: None,
474                        rpc_urls: Some(vec!["https://horizon-testnet.stellar.org".to_string()]),
475                        explorer_urls: None,
476                        average_blocktime_ms: Some(5000),
477                        is_testnet: Some(true),
478                        tags: None,
479                    },
480                    passphrase: Some("Test SDF Network ; September 2015".to_string()),
481                }),
482            };
483
484            self.network_repository.create(test_network).await.unwrap();
485        }
486    }
487
488    #[tokio::test]
489    async fn test_sync_sequence_success() {
490        let ctx = TestCtx::default();
491        ctx.setup_network().await;
492        let relayer_model = ctx.relayer_model.clone();
493        let mut provider = MockStellarProviderTrait::new();
494        provider
495            .expect_get_account()
496            .with(eq(relayer_model.address.clone()))
497            .returning(|_| {
498                Box::pin(async {
499                    Ok(AccountEntry {
500                        account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
501                        balance: 0,
502                        ext: AccountEntryExt::V0,
503                        flags: 0,
504                        home_domain: String32::default(),
505                        inflation_dest: None,
506                        seq_num: SequenceNumber(5),
507                        num_sub_entries: 0,
508                        signers: VecM::default(),
509                        thresholds: Thresholds([0, 0, 0, 0]),
510                    })
511                })
512            });
513        let mut counter = MockTransactionCounterServiceTrait::new();
514        counter
515            .expect_set()
516            .with(eq(6u64))
517            .returning(|_| Box::pin(async { Ok(()) }));
518        let relayer_repo = MockRelayerRepository::new();
519        let tx_repo = MockTransactionRepository::new();
520        let job_producer = MockJobProducerTrait::new();
521
522        let relayer = StellarRelayer::new(
523            relayer_model.clone(),
524            provider,
525            StellarRelayerDependencies::new(
526                Arc::new(relayer_repo),
527                ctx.network_repository.clone(),
528                Arc::new(tx_repo),
529                Arc::new(counter),
530                Arc::new(job_producer),
531            ),
532        )
533        .await
534        .unwrap();
535
536        let result = relayer.sync_sequence().await;
537        assert!(result.is_ok());
538    }
539
540    #[tokio::test]
541    async fn test_sync_sequence_provider_error() {
542        let ctx = TestCtx::default();
543        ctx.setup_network().await;
544        let relayer_model = ctx.relayer_model.clone();
545        let mut provider = MockStellarProviderTrait::new();
546        provider
547            .expect_get_account()
548            .with(eq(relayer_model.address.clone()))
549            .returning(|_| Box::pin(async { Err(eyre!("fail")) }));
550        let counter = MockTransactionCounterServiceTrait::new();
551        let relayer_repo = MockRelayerRepository::new();
552        let tx_repo = MockTransactionRepository::new();
553        let job_producer = MockJobProducerTrait::new();
554
555        let relayer = StellarRelayer::new(
556            relayer_model.clone(),
557            provider,
558            StellarRelayerDependencies::new(
559                Arc::new(relayer_repo),
560                ctx.network_repository.clone(),
561                Arc::new(tx_repo),
562                Arc::new(counter),
563                Arc::new(job_producer),
564            ),
565        )
566        .await
567        .unwrap();
568
569        let result = relayer.sync_sequence().await;
570        assert!(matches!(result, Err(RelayerError::ProviderError(_))));
571    }
572
573    #[tokio::test]
574    async fn test_disable_relayer() {
575        let ctx = TestCtx::default();
576        ctx.setup_network().await;
577        let relayer_model = ctx.relayer_model.clone();
578        let provider = MockStellarProviderTrait::new();
579        let mut relayer_repo = MockRelayerRepository::new();
580        let mut updated_model = relayer_model.clone();
581        updated_model.system_disabled = true;
582        relayer_repo
583            .expect_disable_relayer()
584            .with(eq(relayer_model.id.clone()))
585            .returning(move |_| Ok::<RelayerRepoModel, RepositoryError>(updated_model.clone()));
586        let mut job_producer = MockJobProducerTrait::new();
587        job_producer
588            .expect_produce_send_notification_job()
589            .returning(|_, _| Box::pin(async { Ok(()) }));
590        let tx_repo = MockTransactionRepository::new();
591        let counter = MockTransactionCounterServiceTrait::new();
592
593        let relayer = StellarRelayer::new(
594            relayer_model.clone(),
595            provider,
596            StellarRelayerDependencies::new(
597                Arc::new(relayer_repo),
598                ctx.network_repository.clone(),
599                Arc::new(tx_repo),
600                Arc::new(counter),
601                Arc::new(job_producer),
602            ),
603        )
604        .await
605        .unwrap();
606
607        let reasons = vec!["reason1".to_string(), "reason2".to_string()];
608        let result = relayer.disable_relayer(&reasons).await;
609        assert!(result.is_ok());
610    }
611
612    #[tokio::test]
613    async fn test_get_status_success_stellar() {
614        let ctx = TestCtx::default();
615        ctx.setup_network().await;
616        let relayer_model = ctx.relayer_model.clone();
617        let mut provider_mock = MockStellarProviderTrait::new();
618        let mut tx_repo_mock = MockTransactionRepository::new();
619        let relayer_repo_mock = MockRelayerRepository::new();
620        let job_producer_mock = MockJobProducerTrait::new();
621        let counter_mock = MockTransactionCounterServiceTrait::new();
622
623        provider_mock.expect_get_account().times(2).returning(|_| {
624            Box::pin(ready(Ok(AccountEntry {
625                account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
626                balance: 10000000,
627                seq_num: SequenceNumber(12345),
628                ext: AccountEntryExt::V0,
629                flags: 0,
630                home_domain: String32::default(),
631                inflation_dest: None,
632                num_sub_entries: 0,
633                signers: VecM::default(),
634                thresholds: Thresholds([0, 0, 0, 0]),
635            })))
636        });
637
638        tx_repo_mock
639            .expect_find_by_status()
640            .withf(|relayer_id, statuses| {
641                relayer_id == "test-relayer-id"
642                    && statuses == [TransactionStatus::Pending, TransactionStatus::Submitted]
643            })
644            .returning(|_, _| Ok(vec![]) as Result<Vec<TransactionRepoModel>, RepositoryError>)
645            .once();
646
647        let confirmed_tx = TransactionRepoModel {
648            id: "tx1_stellar".to_string(),
649            relayer_id: relayer_model.id.clone(),
650            status: TransactionStatus::Confirmed,
651            confirmed_at: Some("2023-02-01T12:00:00Z".to_string()),
652            ..TransactionRepoModel::default()
653        };
654        tx_repo_mock
655            .expect_find_by_status()
656            .withf(|relayer_id, statuses| {
657                relayer_id == "test-relayer-id" && statuses == [TransactionStatus::Confirmed]
658            })
659            .returning(move |_, _| {
660                Ok(vec![confirmed_tx.clone()]) as Result<Vec<TransactionRepoModel>, RepositoryError>
661            })
662            .once();
663
664        let stellar_relayer = StellarRelayer::new(
665            relayer_model.clone(),
666            provider_mock,
667            StellarRelayerDependencies::new(
668                Arc::new(relayer_repo_mock),
669                ctx.network_repository.clone(),
670                Arc::new(tx_repo_mock),
671                Arc::new(counter_mock),
672                Arc::new(job_producer_mock),
673            ),
674        )
675        .await
676        .unwrap();
677
678        let status = stellar_relayer.get_status().await.unwrap();
679
680        match status {
681            RelayerStatus::Stellar {
682                balance,
683                pending_transactions_count,
684                last_confirmed_transaction_timestamp,
685                system_disabled,
686                paused,
687                sequence_number,
688            } => {
689                assert_eq!(balance, "10000000");
690                assert_eq!(pending_transactions_count, 0);
691                assert_eq!(
692                    last_confirmed_transaction_timestamp,
693                    Some("2023-02-01T12:00:00Z".to_string())
694                );
695                assert_eq!(system_disabled, relayer_model.system_disabled);
696                assert_eq!(paused, relayer_model.paused);
697                assert_eq!(sequence_number, "12345");
698            }
699            _ => panic!("Expected Stellar RelayerStatus"),
700        }
701    }
702
703    #[tokio::test]
704    async fn test_get_status_stellar_provider_error() {
705        let ctx = TestCtx::default();
706        ctx.setup_network().await;
707        let relayer_model = ctx.relayer_model.clone();
708        let mut provider_mock = MockStellarProviderTrait::new();
709        let tx_repo_mock = MockTransactionRepository::new();
710        let relayer_repo_mock = MockRelayerRepository::new();
711        let job_producer_mock = MockJobProducerTrait::new();
712        let counter_mock = MockTransactionCounterServiceTrait::new();
713
714        provider_mock
715            .expect_get_account()
716            .with(eq(relayer_model.address.clone()))
717            .returning(|_| Box::pin(async { Err(eyre!("Stellar provider down")) }));
718
719        let stellar_relayer = StellarRelayer::new(
720            relayer_model.clone(),
721            provider_mock,
722            StellarRelayerDependencies::new(
723                Arc::new(relayer_repo_mock),
724                ctx.network_repository.clone(),
725                Arc::new(tx_repo_mock),
726                Arc::new(counter_mock),
727                Arc::new(job_producer_mock),
728            ),
729        )
730        .await
731        .unwrap();
732
733        let result = stellar_relayer.get_status().await;
734        assert!(result.is_err());
735        match result.err().unwrap() {
736            RelayerError::ProviderError(msg) => {
737                assert!(msg.contains("Failed to get account details"))
738            }
739            _ => panic!("Expected ProviderError for get_account failure"),
740        }
741    }
742
743    #[tokio::test]
744    async fn test_get_balance_success() {
745        let ctx = TestCtx::default();
746        ctx.setup_network().await;
747        let relayer_model = ctx.relayer_model.clone();
748        let mut provider = MockStellarProviderTrait::new();
749        let expected_balance = 100_000_000i64; // 10 XLM in stroops
750
751        provider
752            .expect_get_account()
753            .with(eq(relayer_model.address.clone()))
754            .returning(move |_| {
755                Box::pin(async move {
756                    Ok(AccountEntry {
757                        account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
758                        balance: expected_balance,
759                        ext: AccountEntryExt::V0,
760                        flags: 0,
761                        home_domain: String32::default(),
762                        inflation_dest: None,
763                        seq_num: SequenceNumber(5),
764                        num_sub_entries: 0,
765                        signers: VecM::default(),
766                        thresholds: Thresholds([0, 0, 0, 0]),
767                    })
768                })
769            });
770
771        let relayer_repo = Arc::new(MockRelayerRepository::new());
772        let tx_repo = Arc::new(MockTransactionRepository::new());
773        let job_producer = Arc::new(MockJobProducerTrait::new());
774        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
775
776        let relayer = StellarRelayer::new(
777            relayer_model,
778            provider,
779            StellarRelayerDependencies::new(
780                relayer_repo,
781                ctx.network_repository.clone(),
782                tx_repo,
783                counter,
784                job_producer,
785            ),
786        )
787        .await
788        .unwrap();
789
790        let result = relayer.get_balance().await;
791        assert!(result.is_ok());
792        let balance_response = result.unwrap();
793        assert_eq!(balance_response.balance, expected_balance as u128);
794        assert_eq!(balance_response.unit, STELLAR_SMALLEST_UNIT_NAME);
795    }
796
797    #[tokio::test]
798    async fn test_get_balance_provider_error() {
799        let ctx = TestCtx::default();
800        ctx.setup_network().await;
801        let relayer_model = ctx.relayer_model.clone();
802        let mut provider = MockStellarProviderTrait::new();
803
804        provider
805            .expect_get_account()
806            .with(eq(relayer_model.address.clone()))
807            .returning(|_| Box::pin(async { Err(eyre!("provider failed")) }));
808
809        let relayer_repo = Arc::new(MockRelayerRepository::new());
810        let tx_repo = Arc::new(MockTransactionRepository::new());
811        let job_producer = Arc::new(MockJobProducerTrait::new());
812        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
813
814        let relayer = StellarRelayer::new(
815            relayer_model,
816            provider,
817            StellarRelayerDependencies::new(
818                relayer_repo,
819                ctx.network_repository.clone(),
820                tx_repo,
821                counter,
822                job_producer,
823            ),
824        )
825        .await
826        .unwrap();
827
828        let result = relayer.get_balance().await;
829        assert!(result.is_err());
830        match result.err().unwrap() {
831            RelayerError::ProviderError(msg) => {
832                assert!(msg.contains("Failed to fetch account for balance: provider failed"));
833            }
834            _ => panic!("Unexpected error type"),
835        }
836    }
837}