openzeppelin_relayer/domain/relayer/stellar/
stellar_relayer.rs

1use crate::domain::map_provider_error;
2use crate::domain::relayer::evm::create_error_response;
3/// This module defines the `StellarRelayer` struct and its associated functionality for
4/// interacting with Stellar networks. The `StellarRelayer` is responsible for managing
5/// transactions, synchronizing sequence numbers, and ensuring the relayer's state is
6/// consistent with the Stellar blockchain.
7///
8/// # Components
9///
10/// - `StellarRelayer`: The main struct that encapsulates the relayer's state and operations for Stellar.
11/// - `RelayerRepoModel`: Represents the relayer's data model.
12/// - `StellarProvider`: Provides blockchain interaction capabilities, such as fetching account details.
13/// - `TransactionCounterService`: Manages the sequence number for transactions to ensure correct ordering.
14/// - `JobProducer`: Produces jobs for processing transactions and sending notifications.
15///
16/// # Error Handling
17///
18/// The module uses the `RelayerError` enum to handle various errors that can occur during
19/// operations, such as provider errors, sequence synchronization failures, and transaction failures.
20///
21/// # Usage
22///
23/// To use the `StellarRelayer`, create an instance using the `new` method, providing the necessary
24/// components. Then, call the appropriate methods to process transactions and manage the relayer's state.
25use crate::{
26    constants::{STELLAR_SMALLEST_UNIT_NAME, STELLAR_STATUS_CHECK_INITIAL_DELAY_SECONDS},
27    domain::{
28        create_success_response, transaction::stellar::fetch_next_sequence_from_chain,
29        BalanceResponse, SignDataRequest, SignDataResponse, SignTransactionExternalResponse,
30        SignTransactionExternalResponseStellar, SignTransactionRequest, SignTypedDataRequest,
31    },
32    jobs::{JobProducerTrait, RelayerHealthCheck, TransactionRequest, TransactionStatusCheck},
33    models::{
34        produce_relayer_disabled_payload, DeletePendingTransactionsResponse, DisabledReason,
35        HealthCheckFailure, JsonRpcRequest, JsonRpcResponse, NetworkRepoModel, NetworkRpcRequest,
36        NetworkRpcResult, NetworkTransactionRequest, NetworkType, RelayerRepoModel, RelayerStatus,
37        RepositoryError, RpcErrorCodes, StellarNetwork, StellarRpcRequest, TransactionRepoModel,
38        TransactionStatus,
39    },
40    repositories::{NetworkRepository, RelayerRepository, Repository, TransactionRepository},
41    services::{
42        provider::{StellarProvider, StellarProviderTrait},
43        signer::{StellarSignTrait, StellarSigner},
44        TransactionCounterService, TransactionCounterServiceTrait,
45    },
46    utils::calculate_scheduled_timestamp,
47};
48use async_trait::async_trait;
49use eyre::Result;
50use std::sync::Arc;
51use tracing::{debug, info, warn};
52
53use crate::domain::relayer::{Relayer, RelayerError};
54
55/// Dependencies container for `StellarRelayer` construction.
56pub struct StellarRelayerDependencies<RR, NR, TR, J, TCS>
57where
58    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
59    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
60    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
61    J: JobProducerTrait + Send + Sync + 'static,
62    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
63{
64    pub relayer_repository: Arc<RR>,
65    pub network_repository: Arc<NR>,
66    pub transaction_repository: Arc<TR>,
67    pub transaction_counter_service: Arc<TCS>,
68    pub job_producer: Arc<J>,
69}
70
71impl<RR, NR, TR, J, TCS> StellarRelayerDependencies<RR, NR, TR, J, TCS>
72where
73    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
74    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
75    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
76    J: JobProducerTrait + Send + Sync,
77    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
78{
79    /// Creates a new dependencies container for `StellarRelayer`.
80    ///
81    /// # Arguments
82    ///
83    /// * `relayer_repository` - Repository for managing relayer model persistence
84    /// * `network_repository` - Repository for accessing network configuration data (RPC URLs, chain settings)
85    /// * `transaction_repository` - Repository for storing and retrieving transaction models
86    /// * `transaction_counter_service` - Service for managing sequence numbers to ensure proper transaction ordering
87    /// * `job_producer` - Service for creating background jobs for transaction processing and notifications
88    ///
89    /// # Returns
90    ///
91    /// Returns a new `StellarRelayerDependencies` instance containing all provided dependencies.
92    pub fn new(
93        relayer_repository: Arc<RR>,
94        network_repository: Arc<NR>,
95        transaction_repository: Arc<TR>,
96        transaction_counter_service: Arc<TCS>,
97        job_producer: Arc<J>,
98    ) -> Self {
99        Self {
100            relayer_repository,
101            network_repository,
102            transaction_repository,
103            transaction_counter_service,
104            job_producer,
105        }
106    }
107}
108
109#[allow(dead_code)]
110pub struct StellarRelayer<P, RR, NR, TR, J, TCS, S>
111where
112    P: StellarProviderTrait + Send + Sync,
113    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
114    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
115    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
116    J: JobProducerTrait + Send + Sync + 'static,
117    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
118    S: StellarSignTrait + Send + Sync + 'static,
119{
120    relayer: RelayerRepoModel,
121    signer: S,
122    network: StellarNetwork,
123    provider: P,
124    relayer_repository: Arc<RR>,
125    network_repository: Arc<NR>,
126    transaction_repository: Arc<TR>,
127    transaction_counter_service: Arc<TCS>,
128    job_producer: Arc<J>,
129}
130
131pub type DefaultStellarRelayer<J, TR, NR, RR, TCR> =
132    StellarRelayer<StellarProvider, RR, NR, TR, J, TransactionCounterService<TCR>, StellarSigner>;
133
134impl<P, RR, NR, TR, J, TCS, S> StellarRelayer<P, RR, NR, TR, J, TCS, S>
135where
136    P: StellarProviderTrait + Send + Sync,
137    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
138    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
139    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
140    J: JobProducerTrait + Send + Sync + 'static,
141    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
142    S: StellarSignTrait + Send + Sync + 'static,
143{
144    /// Creates a new `StellarRelayer` instance.
145    ///
146    /// This constructor initializes a new Stellar relayer with the provided configuration,
147    /// provider, and dependencies. It validates the network configuration and sets up
148    /// all necessary components for transaction processing.
149    ///
150    /// # Arguments
151    ///
152    /// * `relayer` - The relayer model containing configuration like ID, address, network name, and policies
153    /// * `signer` - The Stellar signer for signing transactions
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        signer: S,
165        provider: P,
166        dependencies: StellarRelayerDependencies<RR, NR, TR, J, TCS>,
167    ) -> Result<Self, RelayerError> {
168        let network_repo = dependencies
169            .network_repository
170            .get_by_name(NetworkType::Stellar, &relayer.network)
171            .await
172            .ok()
173            .flatten()
174            .ok_or_else(|| {
175                RelayerError::NetworkConfiguration(format!("Network {} not found", relayer.network))
176            })?;
177
178        let network = StellarNetwork::try_from(network_repo)?;
179
180        Ok(Self {
181            relayer,
182            signer,
183            network,
184            provider,
185            relayer_repository: dependencies.relayer_repository,
186            network_repository: dependencies.network_repository,
187            transaction_repository: dependencies.transaction_repository,
188            transaction_counter_service: dependencies.transaction_counter_service,
189            job_producer: dependencies.job_producer,
190        })
191    }
192
193    async fn sync_sequence(&self) -> Result<(), RelayerError> {
194        info!(
195            "Syncing sequence for relayer: {} ({})",
196            self.relayer.id, self.relayer.address
197        );
198
199        let next = fetch_next_sequence_from_chain(&self.provider, &self.relayer.address)
200            .await
201            .map_err(RelayerError::ProviderError)?;
202
203        info!(
204            "Setting next sequence {} for relayer {}",
205            next, self.relayer.id
206        );
207        self.transaction_counter_service
208            .set(next)
209            .await
210            .map_err(RelayerError::from)?;
211        Ok(())
212    }
213}
214
215#[async_trait]
216impl<P, RR, NR, TR, J, TCS, S> Relayer for StellarRelayer<P, RR, NR, TR, J, TCS, S>
217where
218    P: StellarProviderTrait + Send + Sync,
219    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
220    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
221    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
222    J: JobProducerTrait + Send + Sync + 'static,
223    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
224    S: StellarSignTrait + Send + Sync + 'static,
225{
226    async fn process_transaction_request(
227        &self,
228        network_transaction: NetworkTransactionRequest,
229    ) -> Result<TransactionRepoModel, RelayerError> {
230        let network_model = self
231            .network_repository
232            .get_by_name(NetworkType::Stellar, &self.relayer.network)
233            .await?
234            .ok_or_else(|| {
235                RelayerError::NetworkConfiguration(format!(
236                    "Network {} not found",
237                    self.relayer.network
238                ))
239            })?;
240        let transaction =
241            TransactionRepoModel::try_from((&network_transaction, &self.relayer, &network_model))?;
242
243        self.transaction_repository
244            .create(transaction.clone())
245            .await
246            .map_err(|e| RepositoryError::TransactionFailure(e.to_string()))?;
247
248        self.job_producer
249            .produce_transaction_request_job(
250                TransactionRequest::new(transaction.id.clone(), transaction.relayer_id.clone()),
251                None,
252            )
253            .await?;
254
255        self.job_producer
256            .produce_check_transaction_status_job(
257                TransactionStatusCheck::new(
258                    transaction.id.clone(),
259                    transaction.relayer_id.clone(),
260                    crate::models::NetworkType::Stellar,
261                ),
262                Some(calculate_scheduled_timestamp(
263                    STELLAR_STATUS_CHECK_INITIAL_DELAY_SECONDS,
264                )),
265            )
266            .await?;
267
268        Ok(transaction)
269    }
270
271    async fn get_balance(&self) -> Result<BalanceResponse, RelayerError> {
272        let account_entry = self
273            .provider
274            .get_account(&self.relayer.address)
275            .await
276            .map_err(|e| {
277                RelayerError::ProviderError(format!("Failed to fetch account for balance: {e}"))
278            })?;
279
280        Ok(BalanceResponse {
281            balance: account_entry.balance as u128,
282            unit: STELLAR_SMALLEST_UNIT_NAME.to_string(),
283        })
284    }
285
286    async fn get_status(&self) -> Result<RelayerStatus, RelayerError> {
287        let relayer_model = &self.relayer;
288
289        let account_entry = self
290            .provider
291            .get_account(&relayer_model.address)
292            .await
293            .map_err(|e| {
294                RelayerError::ProviderError(format!("Failed to get account details: {e}"))
295            })?;
296
297        let sequence_number_str = account_entry.seq_num.0.to_string();
298
299        let balance_response = self.get_balance().await?;
300
301        let pending_statuses = [TransactionStatus::Pending, TransactionStatus::Submitted];
302        let pending_transactions = self
303            .transaction_repository
304            .find_by_status(&relayer_model.id, &pending_statuses[..])
305            .await
306            .map_err(RelayerError::from)?;
307        let pending_transactions_count = pending_transactions.len() as u64;
308
309        let confirmed_statuses = [TransactionStatus::Confirmed];
310        let confirmed_transactions = self
311            .transaction_repository
312            .find_by_status(&relayer_model.id, &confirmed_statuses[..])
313            .await
314            .map_err(RelayerError::from)?;
315
316        let last_confirmed_transaction_timestamp = confirmed_transactions
317            .iter()
318            .filter_map(|tx| tx.confirmed_at.as_ref())
319            .max()
320            .cloned();
321
322        Ok(RelayerStatus::Stellar {
323            balance: balance_response.balance.to_string(),
324            pending_transactions_count,
325            last_confirmed_transaction_timestamp,
326            system_disabled: relayer_model.system_disabled,
327            paused: relayer_model.paused,
328            sequence_number: sequence_number_str,
329        })
330    }
331
332    async fn delete_pending_transactions(
333        &self,
334    ) -> Result<DeletePendingTransactionsResponse, RelayerError> {
335        println!("Stellar delete_pending_transactions...");
336        Ok(DeletePendingTransactionsResponse {
337            queued_for_cancellation_transaction_ids: vec![],
338            failed_to_queue_transaction_ids: vec![],
339            total_processed: 0,
340        })
341    }
342
343    async fn sign_data(&self, _request: SignDataRequest) -> Result<SignDataResponse, RelayerError> {
344        Err(RelayerError::NotSupported(
345            "Signing data not supported for Stellar".to_string(),
346        ))
347    }
348
349    async fn sign_typed_data(
350        &self,
351        _request: SignTypedDataRequest,
352    ) -> Result<SignDataResponse, RelayerError> {
353        Err(RelayerError::NotSupported(
354            "Signing typed data not supported for Stellar".to_string(),
355        ))
356    }
357
358    async fn rpc(
359        &self,
360        request: JsonRpcRequest<NetworkRpcRequest>,
361    ) -> Result<JsonRpcResponse<NetworkRpcResult>, RelayerError> {
362        let JsonRpcRequest { id, params, .. } = request;
363        let stellar_request = match params {
364            NetworkRpcRequest::Stellar(stellar_req) => stellar_req,
365            _ => {
366                return Ok(create_error_response(
367                    id.clone(),
368                    RpcErrorCodes::INVALID_PARAMS,
369                    "Invalid params",
370                    "Expected Stellar network request",
371                ))
372            }
373        };
374
375        // Parse method and params from the Stellar request (single unified variant)
376        let (method, params_json) = match stellar_request {
377            StellarRpcRequest::RawRpcRequest { method, params } => (method, params),
378        };
379
380        match self
381            .provider
382            .raw_request_dyn(&method, params_json, id.clone())
383            .await
384        {
385            Ok(result_value) => Ok(create_success_response(id.clone(), result_value)),
386            Err(provider_error) => {
387                let (error_code, error_message) = map_provider_error(&provider_error);
388                Ok(create_error_response(
389                    id.clone(),
390                    error_code,
391                    error_message,
392                    &provider_error.to_string(),
393                ))
394            }
395        }
396    }
397
398    async fn validate_min_balance(&self) -> Result<(), RelayerError> {
399        Ok(())
400    }
401
402    async fn check_health(&self) -> Result<(), Vec<HealthCheckFailure>> {
403        debug!(
404            "running health checks for Stellar relayer {}",
405            self.relayer.id
406        );
407
408        match self.sync_sequence().await {
409            Ok(_) => {
410                debug!(
411                    "all health checks passed for Stellar relayer {}",
412                    self.relayer.id
413                );
414                Ok(())
415            }
416            Err(e) => {
417                let reason = HealthCheckFailure::SequenceSyncFailed(e.to_string());
418                warn!("health checks failed: {:?}", reason);
419                Err(vec![reason])
420            }
421        }
422    }
423
424    async fn initialize_relayer(&self) -> Result<(), RelayerError> {
425        debug!("initializing Stellar relayer {}", self.relayer.id);
426
427        match self.check_health().await {
428            Ok(_) => {
429                // All checks passed
430                if self.relayer.system_disabled {
431                    // Silently re-enable if was disabled (startup, not recovery)
432                    self.relayer_repository
433                        .enable_relayer(self.relayer.id.clone())
434                        .await?;
435                }
436
437                info!(
438                    "Stellar relayer initialized successfully: {}",
439                    self.relayer.id
440                );
441                Ok(())
442            }
443            Err(failures) => {
444                // Health checks failed
445                let reason = DisabledReason::from_health_failures(failures).unwrap_or_else(|| {
446                    DisabledReason::SequenceSyncFailed("Unknown error".to_string())
447                });
448
449                warn!(reason = %reason, "disabling relayer");
450                let updated_relayer = self
451                    .relayer_repository
452                    .disable_relayer(self.relayer.id.clone(), reason.clone())
453                    .await?;
454
455                // Send notification if configured
456                if let Some(notification_id) = &self.relayer.notification_id {
457                    self.job_producer
458                        .produce_send_notification_job(
459                            produce_relayer_disabled_payload(
460                                notification_id,
461                                &updated_relayer,
462                                &reason.safe_description(),
463                            ),
464                            None,
465                        )
466                        .await?;
467                }
468
469                // Schedule health check to try re-enabling the relayer after 10 seconds
470                self.job_producer
471                    .produce_relayer_health_check_job(
472                        RelayerHealthCheck::new(self.relayer.id.clone()),
473                        Some(calculate_scheduled_timestamp(10)),
474                    )
475                    .await?;
476
477                Ok(())
478            }
479        }
480    }
481
482    async fn sign_transaction(
483        &self,
484        request: &SignTransactionRequest,
485    ) -> Result<SignTransactionExternalResponse, RelayerError> {
486        let stellar_req = match request {
487            SignTransactionRequest::Stellar(req) => req,
488            _ => {
489                return Err(RelayerError::NotSupported(
490                    "Invalid request type for Stellar relayer".to_string(),
491                ))
492            }
493        };
494
495        // Use the signer's sign_xdr_transaction method
496        let response = self
497            .signer
498            .sign_xdr_transaction(&stellar_req.unsigned_xdr, &self.network.passphrase)
499            .await
500            .map_err(RelayerError::SignerError)?;
501
502        // Convert DecoratedSignature to base64 string
503        let signature_bytes = &response.signature.signature.0;
504        let signature_string =
505            base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature_bytes);
506
507        Ok(SignTransactionExternalResponse::Stellar(
508            SignTransactionExternalResponseStellar {
509                signed_xdr: response.signed_xdr,
510                signature: signature_string,
511            },
512        ))
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use crate::{
520        config::{NetworkConfigCommon, StellarNetworkConfig},
521        constants::STELLAR_SMALLEST_UNIT_NAME,
522        domain::{SignTransactionRequestStellar, SignXdrTransactionResponseStellar},
523        jobs::MockJobProducerTrait,
524        models::{
525            NetworkConfigData, NetworkRepoModel, NetworkType, RelayerNetworkPolicy,
526            RelayerRepoModel, RelayerStellarPolicy, SignerError,
527        },
528        repositories::{
529            InMemoryNetworkRepository, MockRelayerRepository, MockTransactionRepository,
530        },
531        services::{
532            provider::{MockStellarProviderTrait, ProviderError},
533            signer::MockStellarSignTrait,
534            MockTransactionCounterServiceTrait,
535        },
536    };
537    use mockall::predicate::*;
538    use soroban_rs::xdr::{
539        AccountEntry, AccountEntryExt, AccountId, DecoratedSignature, PublicKey, SequenceNumber,
540        Signature, SignatureHint, String32, Thresholds, Uint256, VecM,
541    };
542    use std::future::ready;
543    use std::sync::Arc;
544
545    /// Test context structure to manage test dependencies
546    struct TestCtx {
547        relayer_model: RelayerRepoModel,
548        network_repository: Arc<InMemoryNetworkRepository>,
549    }
550
551    impl Default for TestCtx {
552        fn default() -> Self {
553            let network_repository = Arc::new(InMemoryNetworkRepository::new());
554
555            let relayer_model = RelayerRepoModel {
556                id: "test-relayer-id".to_string(),
557                name: "Test Relayer".to_string(),
558                network: "testnet".to_string(),
559                paused: false,
560                network_type: NetworkType::Stellar,
561                signer_id: "signer-id".to_string(),
562                policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
563                address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
564                notification_id: Some("notification-id".to_string()),
565                system_disabled: false,
566                custom_rpc_urls: None,
567                ..Default::default()
568            };
569
570            TestCtx {
571                relayer_model,
572                network_repository,
573            }
574        }
575    }
576
577    impl TestCtx {
578        async fn setup_network(&self) {
579            let test_network = NetworkRepoModel {
580                id: "stellar:testnet".to_string(),
581                name: "testnet".to_string(),
582                network_type: NetworkType::Stellar,
583                config: NetworkConfigData::Stellar(StellarNetworkConfig {
584                    common: NetworkConfigCommon {
585                        network: "testnet".to_string(),
586                        from: None,
587                        rpc_urls: Some(vec!["https://horizon-testnet.stellar.org".to_string()]),
588                        explorer_urls: None,
589                        average_blocktime_ms: Some(5000),
590                        is_testnet: Some(true),
591                        tags: None,
592                    },
593                    passphrase: Some("Test SDF Network ; September 2015".to_string()),
594                }),
595            };
596
597            self.network_repository.create(test_network).await.unwrap();
598        }
599    }
600
601    #[tokio::test]
602    async fn test_sync_sequence_success() {
603        let ctx = TestCtx::default();
604        ctx.setup_network().await;
605        let relayer_model = ctx.relayer_model.clone();
606        let mut provider = MockStellarProviderTrait::new();
607        provider
608            .expect_get_account()
609            .with(eq(relayer_model.address.clone()))
610            .returning(|_| {
611                Box::pin(async {
612                    Ok(AccountEntry {
613                        account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
614                        balance: 0,
615                        ext: AccountEntryExt::V0,
616                        flags: 0,
617                        home_domain: String32::default(),
618                        inflation_dest: None,
619                        seq_num: SequenceNumber(5),
620                        num_sub_entries: 0,
621                        signers: VecM::default(),
622                        thresholds: Thresholds([0, 0, 0, 0]),
623                    })
624                })
625            });
626        let mut counter = MockTransactionCounterServiceTrait::new();
627        counter
628            .expect_set()
629            .with(eq(6u64))
630            .returning(|_| Box::pin(async { Ok(()) }));
631        let relayer_repo = MockRelayerRepository::new();
632        let tx_repo = MockTransactionRepository::new();
633        let job_producer = MockJobProducerTrait::new();
634        let signer = MockStellarSignTrait::new();
635
636        let relayer = StellarRelayer::new(
637            relayer_model.clone(),
638            signer,
639            provider,
640            StellarRelayerDependencies::new(
641                Arc::new(relayer_repo),
642                ctx.network_repository.clone(),
643                Arc::new(tx_repo),
644                Arc::new(counter),
645                Arc::new(job_producer),
646            ),
647        )
648        .await
649        .unwrap();
650
651        let result = relayer.sync_sequence().await;
652        assert!(result.is_ok());
653    }
654
655    #[tokio::test]
656    async fn test_sync_sequence_provider_error() {
657        let ctx = TestCtx::default();
658        ctx.setup_network().await;
659        let relayer_model = ctx.relayer_model.clone();
660        let mut provider = MockStellarProviderTrait::new();
661        provider
662            .expect_get_account()
663            .with(eq(relayer_model.address.clone()))
664            .returning(|_| Box::pin(async { Err(ProviderError::Other("fail".to_string())) }));
665        let counter = MockTransactionCounterServiceTrait::new();
666        let relayer_repo = MockRelayerRepository::new();
667        let tx_repo = MockTransactionRepository::new();
668        let job_producer = MockJobProducerTrait::new();
669        let signer = MockStellarSignTrait::new();
670
671        let relayer = StellarRelayer::new(
672            relayer_model.clone(),
673            signer,
674            provider,
675            StellarRelayerDependencies::new(
676                Arc::new(relayer_repo),
677                ctx.network_repository.clone(),
678                Arc::new(tx_repo),
679                Arc::new(counter),
680                Arc::new(job_producer),
681            ),
682        )
683        .await
684        .unwrap();
685
686        let result = relayer.sync_sequence().await;
687        assert!(matches!(result, Err(RelayerError::ProviderError(_))));
688    }
689
690    #[tokio::test]
691    async fn test_get_status_success_stellar() {
692        let ctx = TestCtx::default();
693        ctx.setup_network().await;
694        let relayer_model = ctx.relayer_model.clone();
695        let mut provider_mock = MockStellarProviderTrait::new();
696        let mut tx_repo_mock = MockTransactionRepository::new();
697        let relayer_repo_mock = MockRelayerRepository::new();
698        let job_producer_mock = MockJobProducerTrait::new();
699        let counter_mock = MockTransactionCounterServiceTrait::new();
700
701        provider_mock.expect_get_account().times(2).returning(|_| {
702            Box::pin(ready(Ok(AccountEntry {
703                account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
704                balance: 10000000,
705                seq_num: SequenceNumber(12345),
706                ext: AccountEntryExt::V0,
707                flags: 0,
708                home_domain: String32::default(),
709                inflation_dest: None,
710                num_sub_entries: 0,
711                signers: VecM::default(),
712                thresholds: Thresholds([0, 0, 0, 0]),
713            })))
714        });
715
716        tx_repo_mock
717            .expect_find_by_status()
718            .withf(|relayer_id, statuses| {
719                relayer_id == "test-relayer-id"
720                    && statuses == [TransactionStatus::Pending, TransactionStatus::Submitted]
721            })
722            .returning(|_, _| Ok(vec![]) as Result<Vec<TransactionRepoModel>, RepositoryError>)
723            .once();
724
725        let confirmed_tx = TransactionRepoModel {
726            id: "tx1_stellar".to_string(),
727            relayer_id: relayer_model.id.clone(),
728            status: TransactionStatus::Confirmed,
729            confirmed_at: Some("2023-02-01T12:00:00Z".to_string()),
730            ..TransactionRepoModel::default()
731        };
732        tx_repo_mock
733            .expect_find_by_status()
734            .withf(|relayer_id, statuses| {
735                relayer_id == "test-relayer-id" && statuses == [TransactionStatus::Confirmed]
736            })
737            .returning(move |_, _| {
738                Ok(vec![confirmed_tx.clone()]) as Result<Vec<TransactionRepoModel>, RepositoryError>
739            })
740            .once();
741        let signer = MockStellarSignTrait::new();
742
743        let stellar_relayer = StellarRelayer::new(
744            relayer_model.clone(),
745            signer,
746            provider_mock,
747            StellarRelayerDependencies::new(
748                Arc::new(relayer_repo_mock),
749                ctx.network_repository.clone(),
750                Arc::new(tx_repo_mock),
751                Arc::new(counter_mock),
752                Arc::new(job_producer_mock),
753            ),
754        )
755        .await
756        .unwrap();
757
758        let status = stellar_relayer.get_status().await.unwrap();
759
760        match status {
761            RelayerStatus::Stellar {
762                balance,
763                pending_transactions_count,
764                last_confirmed_transaction_timestamp,
765                system_disabled,
766                paused,
767                sequence_number,
768            } => {
769                assert_eq!(balance, "10000000");
770                assert_eq!(pending_transactions_count, 0);
771                assert_eq!(
772                    last_confirmed_transaction_timestamp,
773                    Some("2023-02-01T12:00:00Z".to_string())
774                );
775                assert_eq!(system_disabled, relayer_model.system_disabled);
776                assert_eq!(paused, relayer_model.paused);
777                assert_eq!(sequence_number, "12345");
778            }
779            _ => panic!("Expected Stellar RelayerStatus"),
780        }
781    }
782
783    #[tokio::test]
784    async fn test_get_status_stellar_provider_error() {
785        let ctx = TestCtx::default();
786        ctx.setup_network().await;
787        let relayer_model = ctx.relayer_model.clone();
788        let mut provider_mock = MockStellarProviderTrait::new();
789        let tx_repo_mock = MockTransactionRepository::new();
790        let relayer_repo_mock = MockRelayerRepository::new();
791        let job_producer_mock = MockJobProducerTrait::new();
792        let counter_mock = MockTransactionCounterServiceTrait::new();
793
794        provider_mock
795            .expect_get_account()
796            .with(eq(relayer_model.address.clone()))
797            .returning(|_| {
798                Box::pin(async { Err(ProviderError::Other("Stellar provider down".to_string())) })
799            });
800        let signer = MockStellarSignTrait::new();
801
802        let stellar_relayer = StellarRelayer::new(
803            relayer_model.clone(),
804            signer,
805            provider_mock,
806            StellarRelayerDependencies::new(
807                Arc::new(relayer_repo_mock),
808                ctx.network_repository.clone(),
809                Arc::new(tx_repo_mock),
810                Arc::new(counter_mock),
811                Arc::new(job_producer_mock),
812            ),
813        )
814        .await
815        .unwrap();
816
817        let result = stellar_relayer.get_status().await;
818        assert!(result.is_err());
819        match result.err().unwrap() {
820            RelayerError::ProviderError(msg) => {
821                assert!(msg.contains("Failed to get account details"))
822            }
823            _ => panic!("Expected ProviderError for get_account failure"),
824        }
825    }
826
827    #[tokio::test]
828    async fn test_get_balance_success() {
829        let ctx = TestCtx::default();
830        ctx.setup_network().await;
831        let relayer_model = ctx.relayer_model.clone();
832        let mut provider = MockStellarProviderTrait::new();
833        let expected_balance = 100_000_000i64; // 10 XLM in stroops
834
835        provider
836            .expect_get_account()
837            .with(eq(relayer_model.address.clone()))
838            .returning(move |_| {
839                Box::pin(async move {
840                    Ok(AccountEntry {
841                        account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
842                        balance: expected_balance,
843                        ext: AccountEntryExt::V0,
844                        flags: 0,
845                        home_domain: String32::default(),
846                        inflation_dest: None,
847                        seq_num: SequenceNumber(5),
848                        num_sub_entries: 0,
849                        signers: VecM::default(),
850                        thresholds: Thresholds([0, 0, 0, 0]),
851                    })
852                })
853            });
854
855        let relayer_repo = Arc::new(MockRelayerRepository::new());
856        let tx_repo = Arc::new(MockTransactionRepository::new());
857        let job_producer = Arc::new(MockJobProducerTrait::new());
858        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
859        let signer = MockStellarSignTrait::new();
860
861        let relayer = StellarRelayer::new(
862            relayer_model,
863            signer,
864            provider,
865            StellarRelayerDependencies::new(
866                relayer_repo,
867                ctx.network_repository.clone(),
868                tx_repo,
869                counter,
870                job_producer,
871            ),
872        )
873        .await
874        .unwrap();
875
876        let result = relayer.get_balance().await;
877        assert!(result.is_ok());
878        let balance_response = result.unwrap();
879        assert_eq!(balance_response.balance, expected_balance as u128);
880        assert_eq!(balance_response.unit, STELLAR_SMALLEST_UNIT_NAME);
881    }
882
883    #[tokio::test]
884    async fn test_get_balance_provider_error() {
885        let ctx = TestCtx::default();
886        ctx.setup_network().await;
887        let relayer_model = ctx.relayer_model.clone();
888        let mut provider = MockStellarProviderTrait::new();
889
890        provider
891            .expect_get_account()
892            .with(eq(relayer_model.address.clone()))
893            .returning(|_| {
894                Box::pin(async { Err(ProviderError::Other("provider failed".to_string())) })
895            });
896
897        let relayer_repo = Arc::new(MockRelayerRepository::new());
898        let tx_repo = Arc::new(MockTransactionRepository::new());
899        let job_producer = Arc::new(MockJobProducerTrait::new());
900        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
901        let signer = MockStellarSignTrait::new();
902
903        let relayer = StellarRelayer::new(
904            relayer_model,
905            signer,
906            provider,
907            StellarRelayerDependencies::new(
908                relayer_repo,
909                ctx.network_repository.clone(),
910                tx_repo,
911                counter,
912                job_producer,
913            ),
914        )
915        .await
916        .unwrap();
917
918        let result = relayer.get_balance().await;
919        assert!(result.is_err());
920        match result.err().unwrap() {
921            RelayerError::ProviderError(msg) => {
922                assert!(msg.contains("Failed to fetch account for balance"));
923            }
924            _ => panic!("Unexpected error type"),
925        }
926    }
927
928    #[tokio::test]
929    async fn test_sign_transaction_success() {
930        let ctx = TestCtx::default();
931        ctx.setup_network().await;
932        let relayer_model = ctx.relayer_model.clone();
933        let provider = MockStellarProviderTrait::new();
934        let mut signer = MockStellarSignTrait::new();
935
936        let unsigned_xdr = "AAAAAgAAAAD///8AAAAAAAAAAQAAAAAAAAACAAAAAQAAAAAAAAAB";
937        let expected_signed_xdr =
938            "AAAAAgAAAAD///8AAAAAAAABAAAAAAAAAAIAAAABAAAAAAAAAAEAAAABAAAAA...";
939        let expected_signature = DecoratedSignature {
940            hint: SignatureHint([1, 2, 3, 4]),
941            signature: Signature([5u8; 64].try_into().unwrap()),
942        };
943        let expected_signature_for_closure = expected_signature.clone();
944
945        signer
946            .expect_sign_xdr_transaction()
947            .with(eq(unsigned_xdr), eq("Test SDF Network ; September 2015"))
948            .returning(move |_, _| {
949                Ok(SignXdrTransactionResponseStellar {
950                    signed_xdr: expected_signed_xdr.to_string(),
951                    signature: expected_signature_for_closure.clone(),
952                })
953            });
954
955        let relayer_repo = Arc::new(MockRelayerRepository::new());
956        let tx_repo = Arc::new(MockTransactionRepository::new());
957        let job_producer = Arc::new(MockJobProducerTrait::new());
958        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
959
960        let relayer = StellarRelayer::new(
961            relayer_model,
962            signer,
963            provider,
964            StellarRelayerDependencies::new(
965                relayer_repo,
966                ctx.network_repository.clone(),
967                tx_repo,
968                counter,
969                job_producer,
970            ),
971        )
972        .await
973        .unwrap();
974
975        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
976            unsigned_xdr: unsigned_xdr.to_string(),
977        });
978        let result = relayer.sign_transaction(&request).await;
979        assert!(result.is_ok());
980
981        match result.unwrap() {
982            SignTransactionExternalResponse::Stellar(response) => {
983                assert_eq!(response.signed_xdr, expected_signed_xdr);
984                // Compare the base64 encoded signature
985                let expected_signature_base64 = base64::Engine::encode(
986                    &base64::engine::general_purpose::STANDARD,
987                    &expected_signature.signature.0,
988                );
989                assert_eq!(response.signature, expected_signature_base64);
990            }
991            _ => panic!("Expected Stellar response"),
992        }
993    }
994
995    #[tokio::test]
996    async fn test_sign_transaction_signer_error() {
997        let ctx = TestCtx::default();
998        ctx.setup_network().await;
999        let relayer_model = ctx.relayer_model.clone();
1000        let provider = MockStellarProviderTrait::new();
1001        let mut signer = MockStellarSignTrait::new();
1002
1003        let unsigned_xdr = "INVALID_XDR";
1004
1005        signer
1006            .expect_sign_xdr_transaction()
1007            .with(eq(unsigned_xdr), eq("Test SDF Network ; September 2015"))
1008            .returning(|_, _| Err(SignerError::SigningError("Invalid XDR format".to_string())));
1009
1010        let relayer_repo = Arc::new(MockRelayerRepository::new());
1011        let tx_repo = Arc::new(MockTransactionRepository::new());
1012        let job_producer = Arc::new(MockJobProducerTrait::new());
1013        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
1014
1015        let relayer = StellarRelayer::new(
1016            relayer_model,
1017            signer,
1018            provider,
1019            StellarRelayerDependencies::new(
1020                relayer_repo,
1021                ctx.network_repository.clone(),
1022                tx_repo,
1023                counter,
1024                job_producer,
1025            ),
1026        )
1027        .await
1028        .unwrap();
1029
1030        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
1031            unsigned_xdr: unsigned_xdr.to_string(),
1032        });
1033        let result = relayer.sign_transaction(&request).await;
1034        assert!(result.is_err());
1035
1036        match result.err().unwrap() {
1037            RelayerError::SignerError(err) => match err {
1038                SignerError::SigningError(msg) => {
1039                    assert_eq!(msg, "Invalid XDR format");
1040                }
1041                _ => panic!("Expected SigningError"),
1042            },
1043            _ => panic!("Expected RelayerError::SignerError"),
1044        }
1045    }
1046
1047    #[tokio::test]
1048    async fn test_sign_transaction_with_different_network_passphrase() {
1049        let ctx = TestCtx::default();
1050        // Create a custom network with a different passphrase
1051        let custom_network = NetworkRepoModel {
1052            id: "stellar:mainnet".to_string(),
1053            name: "mainnet".to_string(),
1054            network_type: NetworkType::Stellar,
1055            config: NetworkConfigData::Stellar(StellarNetworkConfig {
1056                common: NetworkConfigCommon {
1057                    network: "mainnet".to_string(),
1058                    from: None,
1059                    rpc_urls: Some(vec!["https://horizon.stellar.org".to_string()]),
1060                    explorer_urls: None,
1061                    average_blocktime_ms: Some(5000),
1062                    is_testnet: Some(false),
1063                    tags: None,
1064                },
1065                passphrase: Some("Public Global Stellar Network ; September 2015".to_string()),
1066            }),
1067        };
1068        ctx.network_repository.create(custom_network).await.unwrap();
1069
1070        let mut relayer_model = ctx.relayer_model.clone();
1071        relayer_model.network = "mainnet".to_string();
1072
1073        let provider = MockStellarProviderTrait::new();
1074        let mut signer = MockStellarSignTrait::new();
1075
1076        let unsigned_xdr = "AAAAAgAAAAD///8AAAAAAAAAAQAAAAAAAAACAAAAAQAAAAAAAAAB";
1077        let expected_signature = DecoratedSignature {
1078            hint: SignatureHint([10, 20, 30, 40]),
1079            signature: Signature([15u8; 64].try_into().unwrap()),
1080        };
1081        let expected_signature_for_closure = expected_signature.clone();
1082
1083        signer
1084            .expect_sign_xdr_transaction()
1085            .with(
1086                eq(unsigned_xdr),
1087                eq("Public Global Stellar Network ; September 2015"),
1088            )
1089            .returning(move |_, _| {
1090                Ok(SignXdrTransactionResponseStellar {
1091                    signed_xdr: "mainnet_signed_xdr".to_string(),
1092                    signature: expected_signature_for_closure.clone(),
1093                })
1094            });
1095
1096        let relayer_repo = Arc::new(MockRelayerRepository::new());
1097        let tx_repo = Arc::new(MockTransactionRepository::new());
1098        let job_producer = Arc::new(MockJobProducerTrait::new());
1099        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
1100
1101        let relayer = StellarRelayer::new(
1102            relayer_model,
1103            signer,
1104            provider,
1105            StellarRelayerDependencies::new(
1106                relayer_repo,
1107                ctx.network_repository.clone(),
1108                tx_repo,
1109                counter,
1110                job_producer,
1111            ),
1112        )
1113        .await
1114        .unwrap();
1115
1116        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
1117            unsigned_xdr: unsigned_xdr.to_string(),
1118        });
1119        let result = relayer.sign_transaction(&request).await;
1120        assert!(result.is_ok());
1121
1122        match result.unwrap() {
1123            SignTransactionExternalResponse::Stellar(response) => {
1124                assert_eq!(response.signed_xdr, "mainnet_signed_xdr");
1125                // Convert expected signature to base64 for comparison (just the signature bytes, not the whole struct)
1126                let expected_signature_string = base64::Engine::encode(
1127                    &base64::engine::general_purpose::STANDARD,
1128                    &expected_signature.signature.0,
1129                );
1130                assert_eq!(response.signature, expected_signature_string);
1131            }
1132            _ => panic!("Expected Stellar response"),
1133        }
1134    }
1135
1136    #[tokio::test]
1137    async fn test_initialize_relayer_disables_when_validation_fails() {
1138        let ctx = TestCtx::default();
1139        ctx.setup_network().await;
1140        let mut relayer_model = ctx.relayer_model.clone();
1141        relayer_model.system_disabled = false; // Start as enabled
1142        relayer_model.notification_id = Some("test-notification-id".to_string());
1143
1144        let mut provider = MockStellarProviderTrait::new();
1145        let mut relayer_repo = MockRelayerRepository::new();
1146        let mut job_producer = MockJobProducerTrait::new();
1147
1148        // Mock validation failure - sequence sync fails
1149        provider
1150            .expect_get_account()
1151            .returning(|_| Box::pin(ready(Err(ProviderError::Other("RPC error".to_string())))));
1152
1153        // Mock disable_relayer call
1154        let mut disabled_relayer = relayer_model.clone();
1155        disabled_relayer.system_disabled = true;
1156        relayer_repo
1157            .expect_disable_relayer()
1158            .withf(|id, reason| {
1159                id == "test-relayer-id"
1160                    && matches!(reason, crate::models::DisabledReason::SequenceSyncFailed(_))
1161            })
1162            .returning(move |_, _| Ok(disabled_relayer.clone()));
1163
1164        // Mock notification job production
1165        job_producer
1166            .expect_produce_send_notification_job()
1167            .returning(|_, _| Box::pin(async { Ok(()) }));
1168
1169        // Mock health check job scheduling
1170        job_producer
1171            .expect_produce_relayer_health_check_job()
1172            .returning(|_, _| Box::pin(async { Ok(()) }));
1173
1174        let tx_repo = MockTransactionRepository::new();
1175        let counter = MockTransactionCounterServiceTrait::new();
1176        let signer = MockStellarSignTrait::new();
1177
1178        let relayer = StellarRelayer::new(
1179            relayer_model.clone(),
1180            signer,
1181            provider,
1182            StellarRelayerDependencies::new(
1183                Arc::new(relayer_repo),
1184                ctx.network_repository.clone(),
1185                Arc::new(tx_repo),
1186                Arc::new(counter),
1187                Arc::new(job_producer),
1188            ),
1189        )
1190        .await
1191        .unwrap();
1192
1193        let result = relayer.initialize_relayer().await;
1194        assert!(result.is_ok());
1195    }
1196
1197    #[tokio::test]
1198    async fn test_initialize_relayer_enables_when_validation_passes_and_was_disabled() {
1199        let ctx = TestCtx::default();
1200        ctx.setup_network().await;
1201        let mut relayer_model = ctx.relayer_model.clone();
1202        relayer_model.system_disabled = true; // Start as disabled
1203
1204        let mut provider = MockStellarProviderTrait::new();
1205        let mut relayer_repo = MockRelayerRepository::new();
1206
1207        // Mock successful validations - sequence sync succeeds
1208        provider.expect_get_account().returning(|_| {
1209            Box::pin(ready(Ok(AccountEntry {
1210                account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1211                balance: 1000000000, // 100 XLM
1212                seq_num: SequenceNumber(1),
1213                num_sub_entries: 0,
1214                inflation_dest: None,
1215                flags: 0,
1216                home_domain: String32::default(),
1217                thresholds: Thresholds([0; 4]),
1218                signers: VecM::default(),
1219                ext: AccountEntryExt::V0,
1220            })))
1221        });
1222
1223        // Mock enable_relayer call
1224        let mut enabled_relayer = relayer_model.clone();
1225        enabled_relayer.system_disabled = false;
1226        relayer_repo
1227            .expect_enable_relayer()
1228            .with(eq("test-relayer-id".to_string()))
1229            .returning(move |_| Ok(enabled_relayer.clone()));
1230
1231        let tx_repo = MockTransactionRepository::new();
1232        let mut counter = MockTransactionCounterServiceTrait::new();
1233        counter
1234            .expect_set()
1235            .returning(|_| Box::pin(async { Ok(()) }));
1236        let signer = MockStellarSignTrait::new();
1237        let job_producer = MockJobProducerTrait::new();
1238
1239        let relayer = StellarRelayer::new(
1240            relayer_model.clone(),
1241            signer,
1242            provider,
1243            StellarRelayerDependencies::new(
1244                Arc::new(relayer_repo),
1245                ctx.network_repository.clone(),
1246                Arc::new(tx_repo),
1247                Arc::new(counter),
1248                Arc::new(job_producer),
1249            ),
1250        )
1251        .await
1252        .unwrap();
1253
1254        let result = relayer.initialize_relayer().await;
1255        assert!(result.is_ok());
1256    }
1257
1258    #[tokio::test]
1259    async fn test_initialize_relayer_no_action_when_enabled_and_validation_passes() {
1260        let ctx = TestCtx::default();
1261        ctx.setup_network().await;
1262        let mut relayer_model = ctx.relayer_model.clone();
1263        relayer_model.system_disabled = false; // Start as enabled
1264
1265        let mut provider = MockStellarProviderTrait::new();
1266
1267        // Mock successful validations - sequence sync succeeds
1268        provider.expect_get_account().returning(|_| {
1269            Box::pin(ready(Ok(AccountEntry {
1270                account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1271                balance: 1000000000, // 100 XLM
1272                seq_num: SequenceNumber(1),
1273                num_sub_entries: 0,
1274                inflation_dest: None,
1275                flags: 0,
1276                home_domain: String32::default(),
1277                thresholds: Thresholds([0; 4]),
1278                signers: VecM::default(),
1279                ext: AccountEntryExt::V0,
1280            })))
1281        });
1282
1283        // No repository calls should be made since relayer is already enabled
1284
1285        let tx_repo = MockTransactionRepository::new();
1286        let mut counter = MockTransactionCounterServiceTrait::new();
1287        counter
1288            .expect_set()
1289            .returning(|_| Box::pin(async { Ok(()) }));
1290        let signer = MockStellarSignTrait::new();
1291        let job_producer = MockJobProducerTrait::new();
1292        let relayer_repo = MockRelayerRepository::new();
1293
1294        let relayer = StellarRelayer::new(
1295            relayer_model.clone(),
1296            signer,
1297            provider,
1298            StellarRelayerDependencies::new(
1299                Arc::new(relayer_repo),
1300                ctx.network_repository.clone(),
1301                Arc::new(tx_repo),
1302                Arc::new(counter),
1303                Arc::new(job_producer),
1304            ),
1305        )
1306        .await
1307        .unwrap();
1308
1309        let result = relayer.initialize_relayer().await;
1310        assert!(result.is_ok());
1311    }
1312
1313    #[tokio::test]
1314    async fn test_initialize_relayer_sends_notification_when_disabled() {
1315        let ctx = TestCtx::default();
1316        ctx.setup_network().await;
1317        let mut relayer_model = ctx.relayer_model.clone();
1318        relayer_model.system_disabled = false; // Start as enabled
1319        relayer_model.notification_id = Some("test-notification-id".to_string());
1320
1321        let mut provider = MockStellarProviderTrait::new();
1322        let mut relayer_repo = MockRelayerRepository::new();
1323        let mut job_producer = MockJobProducerTrait::new();
1324
1325        // Mock validation failure - sequence sync fails
1326        provider.expect_get_account().returning(|_| {
1327            Box::pin(ready(Err(ProviderError::Other(
1328                "Sequence sync failed".to_string(),
1329            ))))
1330        });
1331
1332        // Mock disable_relayer call
1333        let mut disabled_relayer = relayer_model.clone();
1334        disabled_relayer.system_disabled = true;
1335        relayer_repo
1336            .expect_disable_relayer()
1337            .withf(|id, reason| {
1338                id == "test-relayer-id"
1339                    && matches!(reason, crate::models::DisabledReason::SequenceSyncFailed(_))
1340            })
1341            .returning(move |_, _| Ok(disabled_relayer.clone()));
1342
1343        // Mock notification job production - verify it's called
1344        job_producer
1345            .expect_produce_send_notification_job()
1346            .returning(|_, _| Box::pin(async { Ok(()) }));
1347
1348        // Mock health check job scheduling
1349        job_producer
1350            .expect_produce_relayer_health_check_job()
1351            .returning(|_, _| Box::pin(async { Ok(()) }));
1352
1353        let tx_repo = MockTransactionRepository::new();
1354        let counter = MockTransactionCounterServiceTrait::new();
1355        let signer = MockStellarSignTrait::new();
1356
1357        let relayer = StellarRelayer::new(
1358            relayer_model.clone(),
1359            signer,
1360            provider,
1361            StellarRelayerDependencies::new(
1362                Arc::new(relayer_repo),
1363                ctx.network_repository.clone(),
1364                Arc::new(tx_repo),
1365                Arc::new(counter),
1366                Arc::new(job_producer),
1367            ),
1368        )
1369        .await
1370        .unwrap();
1371
1372        let result = relayer.initialize_relayer().await;
1373        assert!(result.is_ok());
1374    }
1375
1376    #[tokio::test]
1377    async fn test_initialize_relayer_no_notification_when_no_notification_id() {
1378        let ctx = TestCtx::default();
1379        ctx.setup_network().await;
1380        let mut relayer_model = ctx.relayer_model.clone();
1381        relayer_model.system_disabled = false; // Start as enabled
1382        relayer_model.notification_id = None; // No notification ID
1383
1384        let mut provider = MockStellarProviderTrait::new();
1385        let mut relayer_repo = MockRelayerRepository::new();
1386
1387        // Mock validation failure - sequence sync fails
1388        provider.expect_get_account().returning(|_| {
1389            Box::pin(ready(Err(ProviderError::Other(
1390                "Sequence sync failed".to_string(),
1391            ))))
1392        });
1393
1394        // Mock disable_relayer call
1395        let mut disabled_relayer = relayer_model.clone();
1396        disabled_relayer.system_disabled = true;
1397        relayer_repo
1398            .expect_disable_relayer()
1399            .withf(|id, reason| {
1400                id == "test-relayer-id"
1401                    && matches!(reason, crate::models::DisabledReason::SequenceSyncFailed(_))
1402            })
1403            .returning(move |_, _| Ok(disabled_relayer.clone()));
1404
1405        // No notification job should be produced since notification_id is None
1406        // But health check job should still be scheduled
1407        let mut job_producer = MockJobProducerTrait::new();
1408        job_producer
1409            .expect_produce_relayer_health_check_job()
1410            .returning(|_, _| Box::pin(async { Ok(()) }));
1411
1412        let tx_repo = MockTransactionRepository::new();
1413        let counter = MockTransactionCounterServiceTrait::new();
1414        let signer = MockStellarSignTrait::new();
1415
1416        let relayer = StellarRelayer::new(
1417            relayer_model.clone(),
1418            signer,
1419            provider,
1420            StellarRelayerDependencies::new(
1421                Arc::new(relayer_repo),
1422                ctx.network_repository.clone(),
1423                Arc::new(tx_repo),
1424                Arc::new(counter),
1425                Arc::new(job_producer),
1426            ),
1427        )
1428        .await
1429        .unwrap();
1430
1431        let result = relayer.initialize_relayer().await;
1432        assert!(result.is_ok());
1433    }
1434
1435    mod process_transaction_request_tests {
1436        use super::*;
1437        use crate::constants::STELLAR_STATUS_CHECK_INITIAL_DELAY_SECONDS;
1438        use crate::models::{
1439            NetworkTransactionRequest, NetworkType, StellarTransactionRequest, TransactionStatus,
1440        };
1441        use chrono::Utc;
1442
1443        // Helper function to create a valid test transaction request
1444        fn create_test_transaction_request() -> NetworkTransactionRequest {
1445            NetworkTransactionRequest::Stellar(StellarTransactionRequest {
1446                source_account: None,
1447                network: "testnet".to_string(),
1448                operations: None,
1449                memo: None,
1450                valid_until: None,
1451                transaction_xdr: Some("AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAAEAAHAkAAAADwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=".to_string()),
1452                fee_bump: None,
1453                max_fee: None,
1454            })
1455        }
1456
1457        #[tokio::test]
1458        async fn test_process_transaction_request_calls_job_producer_methods() {
1459            let ctx = TestCtx::default();
1460            ctx.setup_network().await;
1461            let relayer_model = ctx.relayer_model.clone();
1462
1463            let provider = MockStellarProviderTrait::new();
1464            let signer = MockStellarSignTrait::new();
1465
1466            // Create a test transaction request
1467            let tx_request = create_test_transaction_request();
1468
1469            // Mock transaction repository - we expect it to create a transaction
1470            let mut tx_repo = MockTransactionRepository::new();
1471            tx_repo.expect_create().returning(|t| Ok(t.clone()));
1472
1473            // Mock job producer to verify both methods are called
1474            let mut job_producer = MockJobProducerTrait::new();
1475
1476            // Verify produce_transaction_request_job is called
1477            job_producer
1478                .expect_produce_transaction_request_job()
1479                .withf(|req, delay| {
1480                    !req.transaction_id.is_empty() && !req.relayer_id.is_empty() && delay.is_none()
1481                })
1482                .times(1)
1483                .returning(|_, _| Box::pin(async { Ok(()) }));
1484
1485            // Verify produce_check_transaction_status_job is called with correct parameters
1486            job_producer
1487                .expect_produce_check_transaction_status_job()
1488                .withf(|check, delay| {
1489                    !check.transaction_id.is_empty()
1490                        && !check.relayer_id.is_empty()
1491                        && check.network_type == Some(NetworkType::Stellar)
1492                        && delay.is_some()
1493                })
1494                .times(1)
1495                .returning(|_, _| Box::pin(async { Ok(()) }));
1496
1497            let relayer_repo = Arc::new(MockRelayerRepository::new());
1498            let counter = MockTransactionCounterServiceTrait::new();
1499
1500            let relayer = StellarRelayer::new(
1501                relayer_model,
1502                signer,
1503                provider,
1504                StellarRelayerDependencies::new(
1505                    relayer_repo,
1506                    ctx.network_repository.clone(),
1507                    Arc::new(tx_repo),
1508                    Arc::new(counter),
1509                    Arc::new(job_producer),
1510                ),
1511            )
1512            .await
1513            .unwrap();
1514
1515            let result = relayer.process_transaction_request(tx_request).await;
1516            if let Err(e) = &result {
1517                panic!("process_transaction_request failed: {}", e);
1518            }
1519            assert!(result.is_ok());
1520        }
1521
1522        #[tokio::test]
1523        async fn test_process_transaction_request_with_scheduled_delay() {
1524            let ctx = TestCtx::default();
1525            ctx.setup_network().await;
1526            let relayer_model = ctx.relayer_model.clone();
1527
1528            let provider = MockStellarProviderTrait::new();
1529            let signer = MockStellarSignTrait::new();
1530
1531            let tx_request = create_test_transaction_request();
1532
1533            let mut tx_repo = MockTransactionRepository::new();
1534            tx_repo.expect_create().returning(|t| Ok(t.clone()));
1535
1536            let mut job_producer = MockJobProducerTrait::new();
1537
1538            job_producer
1539                .expect_produce_transaction_request_job()
1540                .returning(|_, _| Box::pin(async { Ok(()) }));
1541
1542            // Verify that the status check is scheduled with the initial delay
1543            job_producer
1544                .expect_produce_check_transaction_status_job()
1545                .withf(|_, delay| {
1546                    // Should have a delay timestamp
1547                    if let Some(scheduled_at) = delay {
1548                        // The scheduled time should be approximately STELLAR_STATUS_CHECK_INITIAL_DELAY_SECONDS from now
1549                        let now = Utc::now().timestamp();
1550                        let diff = scheduled_at - now;
1551                        // Allow some tolerance (within 2 seconds)
1552                        diff >= (STELLAR_STATUS_CHECK_INITIAL_DELAY_SECONDS - 2)
1553                            && diff <= (STELLAR_STATUS_CHECK_INITIAL_DELAY_SECONDS + 2)
1554                    } else {
1555                        false
1556                    }
1557                })
1558                .times(1)
1559                .returning(|_, _| Box::pin(async { Ok(()) }));
1560
1561            let relayer_repo = Arc::new(MockRelayerRepository::new());
1562            let counter = MockTransactionCounterServiceTrait::new();
1563
1564            let relayer = StellarRelayer::new(
1565                relayer_model,
1566                signer,
1567                provider,
1568                StellarRelayerDependencies::new(
1569                    relayer_repo,
1570                    ctx.network_repository.clone(),
1571                    Arc::new(tx_repo),
1572                    Arc::new(counter),
1573                    Arc::new(job_producer),
1574                ),
1575            )
1576            .await
1577            .unwrap();
1578
1579            let result = relayer.process_transaction_request(tx_request).await;
1580            assert!(result.is_ok());
1581        }
1582
1583        #[tokio::test]
1584        async fn test_process_transaction_request_repository_failure() {
1585            let ctx = TestCtx::default();
1586            ctx.setup_network().await;
1587            let relayer_model = ctx.relayer_model.clone();
1588
1589            let provider = MockStellarProviderTrait::new();
1590            let signer = MockStellarSignTrait::new();
1591
1592            let tx_request = create_test_transaction_request();
1593
1594            // Mock repository failure
1595            let mut tx_repo = MockTransactionRepository::new();
1596            tx_repo.expect_create().returning(|_| {
1597                Err(RepositoryError::TransactionFailure(
1598                    "Database connection failed".to_string(),
1599                ))
1600            });
1601
1602            // Job producer should NOT be called when repository fails
1603            let job_producer = MockJobProducerTrait::new();
1604
1605            let relayer_repo = Arc::new(MockRelayerRepository::new());
1606            let counter = MockTransactionCounterServiceTrait::new();
1607
1608            let relayer = StellarRelayer::new(
1609                relayer_model,
1610                signer,
1611                provider,
1612                StellarRelayerDependencies::new(
1613                    relayer_repo,
1614                    ctx.network_repository.clone(),
1615                    Arc::new(tx_repo),
1616                    Arc::new(counter),
1617                    Arc::new(job_producer),
1618                ),
1619            )
1620            .await
1621            .unwrap();
1622
1623            let result = relayer.process_transaction_request(tx_request).await;
1624            assert!(result.is_err());
1625            // RepositoryError is converted to RelayerError::NetworkConfiguration
1626            let err_msg = result.err().unwrap().to_string();
1627            assert!(
1628                err_msg.contains("Database connection failed"),
1629                "Error was: {}",
1630                err_msg
1631            );
1632        }
1633
1634        #[tokio::test]
1635        async fn test_process_transaction_request_job_producer_request_failure() {
1636            let ctx = TestCtx::default();
1637            ctx.setup_network().await;
1638            let relayer_model = ctx.relayer_model.clone();
1639
1640            let provider = MockStellarProviderTrait::new();
1641            let signer = MockStellarSignTrait::new();
1642
1643            let tx_request = create_test_transaction_request();
1644
1645            let mut tx_repo = MockTransactionRepository::new();
1646            tx_repo.expect_create().returning(|t| Ok(t.clone()));
1647
1648            // Mock produce_transaction_request_job to fail
1649            let mut job_producer = MockJobProducerTrait::new();
1650            job_producer
1651                .expect_produce_transaction_request_job()
1652                .returning(|_, _| {
1653                    Box::pin(async {
1654                        Err(crate::jobs::JobProducerError::QueueError(
1655                            "Queue is full".to_string(),
1656                        ))
1657                    })
1658                });
1659
1660            // Status check job should NOT be called if request job fails
1661
1662            let relayer_repo = Arc::new(MockRelayerRepository::new());
1663            let counter = MockTransactionCounterServiceTrait::new();
1664
1665            let relayer = StellarRelayer::new(
1666                relayer_model,
1667                signer,
1668                provider,
1669                StellarRelayerDependencies::new(
1670                    relayer_repo,
1671                    ctx.network_repository.clone(),
1672                    Arc::new(tx_repo),
1673                    Arc::new(counter),
1674                    Arc::new(job_producer),
1675                ),
1676            )
1677            .await
1678            .unwrap();
1679
1680            let result = relayer.process_transaction_request(tx_request).await;
1681            assert!(result.is_err());
1682        }
1683
1684        #[tokio::test]
1685        async fn test_process_transaction_request_job_producer_status_check_failure() {
1686            let ctx = TestCtx::default();
1687            ctx.setup_network().await;
1688            let relayer_model = ctx.relayer_model.clone();
1689
1690            let provider = MockStellarProviderTrait::new();
1691            let signer = MockStellarSignTrait::new();
1692
1693            let tx_request = create_test_transaction_request();
1694
1695            let mut tx_repo = MockTransactionRepository::new();
1696            tx_repo.expect_create().returning(|t| Ok(t.clone()));
1697
1698            let mut job_producer = MockJobProducerTrait::new();
1699
1700            // Request job succeeds
1701            job_producer
1702                .expect_produce_transaction_request_job()
1703                .returning(|_, _| Box::pin(async { Ok(()) }));
1704
1705            // Status check job fails
1706            job_producer
1707                .expect_produce_check_transaction_status_job()
1708                .returning(|_, _| {
1709                    Box::pin(async {
1710                        Err(crate::jobs::JobProducerError::QueueError(
1711                            "Failed to queue job".to_string(),
1712                        ))
1713                    })
1714                });
1715
1716            let relayer_repo = Arc::new(MockRelayerRepository::new());
1717            let counter = MockTransactionCounterServiceTrait::new();
1718
1719            let relayer = StellarRelayer::new(
1720                relayer_model,
1721                signer,
1722                provider,
1723                StellarRelayerDependencies::new(
1724                    relayer_repo,
1725                    ctx.network_repository.clone(),
1726                    Arc::new(tx_repo),
1727                    Arc::new(counter),
1728                    Arc::new(job_producer),
1729                ),
1730            )
1731            .await
1732            .unwrap();
1733
1734            let result = relayer.process_transaction_request(tx_request).await;
1735            assert!(result.is_err());
1736        }
1737
1738        #[tokio::test]
1739        async fn test_process_transaction_request_preserves_transaction_data() {
1740            let ctx = TestCtx::default();
1741            ctx.setup_network().await;
1742            let relayer_model = ctx.relayer_model.clone();
1743
1744            let provider = MockStellarProviderTrait::new();
1745            let signer = MockStellarSignTrait::new();
1746
1747            let tx_request = create_test_transaction_request();
1748
1749            let mut tx_repo = MockTransactionRepository::new();
1750            tx_repo.expect_create().returning(|t| Ok(t.clone()));
1751
1752            let mut job_producer = MockJobProducerTrait::new();
1753            job_producer
1754                .expect_produce_transaction_request_job()
1755                .returning(|_, _| Box::pin(async { Ok(()) }));
1756            job_producer
1757                .expect_produce_check_transaction_status_job()
1758                .returning(|_, _| Box::pin(async { Ok(()) }));
1759
1760            let relayer_repo = Arc::new(MockRelayerRepository::new());
1761            let counter = MockTransactionCounterServiceTrait::new();
1762
1763            let relayer = StellarRelayer::new(
1764                relayer_model.clone(),
1765                signer,
1766                provider,
1767                StellarRelayerDependencies::new(
1768                    relayer_repo,
1769                    ctx.network_repository.clone(),
1770                    Arc::new(tx_repo),
1771                    Arc::new(counter),
1772                    Arc::new(job_producer),
1773                ),
1774            )
1775            .await
1776            .unwrap();
1777
1778            let result = relayer.process_transaction_request(tx_request).await;
1779            assert!(result.is_ok());
1780
1781            let returned_tx = result.unwrap();
1782            assert_eq!(returned_tx.relayer_id, relayer_model.id);
1783            assert_eq!(returned_tx.network_type, NetworkType::Stellar);
1784            assert_eq!(returned_tx.status, TransactionStatus::Pending);
1785        }
1786    }
1787}