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        transaction::stellar::fetch_next_sequence_from_chain, BalanceResponse, SignDataRequest,
27        SignDataResponse, SignTransactionExternalResponse, SignTransactionExternalResponseStellar,
28        SignTransactionRequest, SignTypedDataRequest,
29    },
30    jobs::{JobProducerTrait, TransactionRequest},
31    models::{
32        produce_relayer_disabled_payload, DeletePendingTransactionsResponse, JsonRpcRequest,
33        JsonRpcResponse, NetworkRepoModel, NetworkRpcRequest, NetworkRpcResult,
34        NetworkTransactionRequest, NetworkType, RelayerRepoModel, RelayerStatus, RepositoryError,
35        StellarNetwork, StellarRpcResult, TransactionRepoModel, TransactionStatus,
36    },
37    repositories::{NetworkRepository, RelayerRepository, Repository, TransactionRepository},
38    services::{
39        StellarProvider, StellarProviderTrait, StellarSignTrait, StellarSigner,
40        TransactionCounterService, TransactionCounterServiceTrait,
41    },
42};
43use async_trait::async_trait;
44use eyre::Result;
45use log::{info, warn};
46use std::sync::Arc;
47
48use crate::domain::relayer::{Relayer, RelayerError};
49
50/// Dependencies container for `StellarRelayer` construction.
51pub struct StellarRelayerDependencies<RR, NR, TR, J, TCS>
52where
53    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
54    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
55    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
56    J: JobProducerTrait + Send + Sync + 'static,
57    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
58{
59    pub relayer_repository: Arc<RR>,
60    pub network_repository: Arc<NR>,
61    pub transaction_repository: Arc<TR>,
62    pub transaction_counter_service: Arc<TCS>,
63    pub job_producer: Arc<J>,
64}
65
66impl<RR, NR, TR, J, TCS> StellarRelayerDependencies<RR, NR, TR, J, TCS>
67where
68    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
69    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
70    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
71    J: JobProducerTrait + Send + Sync,
72    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
73{
74    /// Creates a new dependencies container for `StellarRelayer`.
75    ///
76    /// # Arguments
77    ///
78    /// * `relayer_repository` - Repository for managing relayer model persistence
79    /// * `network_repository` - Repository for accessing network configuration data (RPC URLs, chain settings)
80    /// * `transaction_repository` - Repository for storing and retrieving transaction models
81    /// * `transaction_counter_service` - Service for managing sequence numbers to ensure proper transaction ordering
82    /// * `job_producer` - Service for creating background jobs for transaction processing and notifications
83    ///
84    /// # Returns
85    ///
86    /// Returns a new `StellarRelayerDependencies` instance containing all provided dependencies.
87    pub fn new(
88        relayer_repository: Arc<RR>,
89        network_repository: Arc<NR>,
90        transaction_repository: Arc<TR>,
91        transaction_counter_service: Arc<TCS>,
92        job_producer: Arc<J>,
93    ) -> Self {
94        Self {
95            relayer_repository,
96            network_repository,
97            transaction_repository,
98            transaction_counter_service,
99            job_producer,
100        }
101    }
102}
103
104#[allow(dead_code)]
105pub struct StellarRelayer<P, RR, NR, TR, J, TCS, S>
106where
107    P: StellarProviderTrait + Send + Sync,
108    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
109    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
110    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
111    J: JobProducerTrait + Send + Sync + 'static,
112    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
113    S: StellarSignTrait + Send + Sync + 'static,
114{
115    relayer: RelayerRepoModel,
116    signer: S,
117    network: StellarNetwork,
118    provider: P,
119    relayer_repository: Arc<RR>,
120    network_repository: Arc<NR>,
121    transaction_repository: Arc<TR>,
122    transaction_counter_service: Arc<TCS>,
123    job_producer: Arc<J>,
124}
125
126pub type DefaultStellarRelayer<J, TR, NR, RR, TCR> =
127    StellarRelayer<StellarProvider, RR, NR, TR, J, TransactionCounterService<TCR>, StellarSigner>;
128
129impl<P, RR, NR, TR, J, TCS, S> StellarRelayer<P, RR, NR, TR, J, TCS, S>
130where
131    P: StellarProviderTrait + Send + Sync,
132    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
133    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
134    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
135    J: JobProducerTrait + Send + Sync + 'static,
136    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
137    S: StellarSignTrait + Send + Sync + 'static,
138{
139    /// Creates a new `StellarRelayer` instance.
140    ///
141    /// This constructor initializes a new Stellar relayer with the provided configuration,
142    /// provider, and dependencies. It validates the network configuration and sets up
143    /// all necessary components for transaction processing.
144    ///
145    /// # Arguments
146    ///
147    /// * `relayer` - The relayer model containing configuration like ID, address, network name, and policies
148    /// * `signer` - The Stellar signer for signing transactions
149    /// * `provider` - The Stellar provider implementation for blockchain interactions (account queries, transaction submission)
150    /// * `dependencies` - Container with all required repositories and services (see [`StellarRelayerDependencies`])
151    ///
152    /// # Returns
153    ///
154    /// * `Ok(StellarRelayer)` - Successfully initialized relayer ready for operation
155    /// * `Err(RelayerError)` - If initialization fails due to configuration or validation errors
156    #[allow(clippy::too_many_arguments)]
157    pub async fn new(
158        relayer: RelayerRepoModel,
159        signer: S,
160        provider: P,
161        dependencies: StellarRelayerDependencies<RR, NR, TR, J, TCS>,
162    ) -> Result<Self, RelayerError> {
163        let network_repo = dependencies
164            .network_repository
165            .get_by_name(NetworkType::Stellar, &relayer.network)
166            .await
167            .ok()
168            .flatten()
169            .ok_or_else(|| {
170                RelayerError::NetworkConfiguration(format!("Network {} not found", relayer.network))
171            })?;
172
173        let network = StellarNetwork::try_from(network_repo)?;
174
175        Ok(Self {
176            relayer,
177            signer,
178            network,
179            provider,
180            relayer_repository: dependencies.relayer_repository,
181            network_repository: dependencies.network_repository,
182            transaction_repository: dependencies.transaction_repository,
183            transaction_counter_service: dependencies.transaction_counter_service,
184            job_producer: dependencies.job_producer,
185        })
186    }
187
188    async fn sync_sequence(&self) -> Result<(), RelayerError> {
189        info!(
190            "Syncing sequence for relayer: {} ({})",
191            self.relayer.id, self.relayer.address
192        );
193
194        let next = fetch_next_sequence_from_chain(&self.provider, &self.relayer.address)
195            .await
196            .map_err(RelayerError::ProviderError)?;
197
198        info!(
199            "Setting next sequence {} for relayer {}",
200            next, self.relayer.id
201        );
202        self.transaction_counter_service
203            .set(next)
204            .await
205            .map_err(RelayerError::from)?;
206        Ok(())
207    }
208
209    async fn disable_relayer(&self, reasons: &[String]) -> Result<(), RelayerError> {
210        let reason = reasons.join(", ");
211        warn!("Disabling relayer {} due to: {}", self.relayer.id, reason);
212
213        let updated = self
214            .relayer_repository
215            .disable_relayer(self.relayer.id.clone())
216            .await?;
217
218        if let Some(nid) = &self.relayer.notification_id {
219            self.job_producer
220                .produce_send_notification_job(
221                    produce_relayer_disabled_payload(nid, &updated, &reason),
222                    None,
223                )
224                .await?;
225        }
226        Ok(())
227    }
228}
229
230#[async_trait]
231impl<P, RR, NR, TR, J, TCS, S> Relayer for StellarRelayer<P, RR, NR, TR, J, TCS, S>
232where
233    P: StellarProviderTrait + Send + Sync,
234    RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
235    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
236    TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
237    J: JobProducerTrait + Send + Sync + 'static,
238    TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
239    S: StellarSignTrait + Send + Sync + 'static,
240{
241    async fn process_transaction_request(
242        &self,
243        network_transaction: NetworkTransactionRequest,
244    ) -> Result<TransactionRepoModel, RelayerError> {
245        let network_model = self
246            .network_repository
247            .get_by_name(NetworkType::Stellar, &self.relayer.network)
248            .await?
249            .ok_or_else(|| {
250                RelayerError::NetworkConfiguration(format!(
251                    "Network {} not found",
252                    self.relayer.network
253                ))
254            })?;
255        let transaction =
256            TransactionRepoModel::try_from((&network_transaction, &self.relayer, &network_model))?;
257
258        self.transaction_repository
259            .create(transaction.clone())
260            .await
261            .map_err(|e| RepositoryError::TransactionFailure(e.to_string()))?;
262
263        self.job_producer
264            .produce_transaction_request_job(
265                TransactionRequest::new(transaction.id.clone(), transaction.relayer_id.clone()),
266                None,
267            )
268            .await?;
269
270        Ok(transaction)
271    }
272
273    async fn get_balance(&self) -> Result<BalanceResponse, RelayerError> {
274        let account_entry = self
275            .provider
276            .get_account(&self.relayer.address)
277            .await
278            .map_err(|e| {
279                RelayerError::ProviderError(format!("Failed to fetch account for balance: {}", e))
280            })?;
281
282        Ok(BalanceResponse {
283            balance: account_entry.balance as u128,
284            unit: STELLAR_SMALLEST_UNIT_NAME.to_string(),
285        })
286    }
287
288    async fn get_status(&self) -> Result<RelayerStatus, RelayerError> {
289        let relayer_model = &self.relayer;
290
291        let account_entry = self
292            .provider
293            .get_account(&relayer_model.address)
294            .await
295            .map_err(|e| {
296                RelayerError::ProviderError(format!("Failed to get account details: {}", e))
297            })?;
298
299        let sequence_number_str = account_entry.seq_num.0.to_string();
300
301        let balance_response = self.get_balance().await?;
302
303        let pending_statuses = [TransactionStatus::Pending, TransactionStatus::Submitted];
304        let pending_transactions = self
305            .transaction_repository
306            .find_by_status(&relayer_model.id, &pending_statuses[..])
307            .await
308            .map_err(RelayerError::from)?;
309        let pending_transactions_count = pending_transactions.len() as u64;
310
311        let confirmed_statuses = [TransactionStatus::Confirmed];
312        let confirmed_transactions = self
313            .transaction_repository
314            .find_by_status(&relayer_model.id, &confirmed_statuses[..])
315            .await
316            .map_err(RelayerError::from)?;
317
318        let last_confirmed_transaction_timestamp = confirmed_transactions
319            .iter()
320            .filter_map(|tx| tx.confirmed_at.as_ref())
321            .max()
322            .cloned();
323
324        Ok(RelayerStatus::Stellar {
325            balance: balance_response.balance.to_string(),
326            pending_transactions_count,
327            last_confirmed_transaction_timestamp,
328            system_disabled: relayer_model.system_disabled,
329            paused: relayer_model.paused,
330            sequence_number: sequence_number_str,
331        })
332    }
333
334    async fn delete_pending_transactions(
335        &self,
336    ) -> Result<DeletePendingTransactionsResponse, RelayerError> {
337        println!("Stellar delete_pending_transactions...");
338        Ok(DeletePendingTransactionsResponse {
339            queued_for_cancellation_transaction_ids: vec![],
340            failed_to_queue_transaction_ids: vec![],
341            total_processed: 0,
342        })
343    }
344
345    async fn sign_data(&self, _request: SignDataRequest) -> Result<SignDataResponse, RelayerError> {
346        Err(RelayerError::NotSupported(
347            "Signing data not supported for Stellar".to_string(),
348        ))
349    }
350
351    async fn sign_typed_data(
352        &self,
353        _request: SignTypedDataRequest,
354    ) -> Result<SignDataResponse, RelayerError> {
355        Err(RelayerError::NotSupported(
356            "Signing typed data not supported for Stellar".to_string(),
357        ))
358    }
359
360    async fn rpc(
361        &self,
362        _request: JsonRpcRequest<NetworkRpcRequest>,
363    ) -> Result<JsonRpcResponse<NetworkRpcResult>, RelayerError> {
364        println!("Stellar rpc...");
365        Ok(JsonRpcResponse {
366            id: None,
367            jsonrpc: "2.0".to_string(),
368            result: Some(NetworkRpcResult::Stellar(
369                StellarRpcResult::GenericRpcResult("".to_string()),
370            )),
371            error: None,
372        })
373    }
374
375    async fn validate_min_balance(&self) -> Result<(), RelayerError> {
376        Ok(())
377    }
378
379    async fn initialize_relayer(&self) -> Result<(), RelayerError> {
380        info!("Initializing Stellar relayer: {}", self.relayer.id);
381
382        let seq_res = self.sync_sequence().await.err();
383
384        let mut failures: Vec<String> = Vec::new();
385        if let Some(e) = seq_res {
386            failures.push(format!("Sequence sync failed: {}", e));
387        }
388
389        if !failures.is_empty() {
390            self.disable_relayer(&failures).await?;
391            return Ok(()); // same semantics as EVM
392        }
393
394        info!(
395            "Stellar relayer initialized successfully: {}",
396            self.relayer.id
397        );
398        Ok(())
399    }
400
401    async fn sign_transaction(
402        &self,
403        request: &SignTransactionRequest,
404    ) -> Result<SignTransactionExternalResponse, RelayerError> {
405        let stellar_req = match request {
406            SignTransactionRequest::Stellar(req) => req,
407            _ => {
408                return Err(RelayerError::NotSupported(
409                    "Invalid request type for Stellar relayer".to_string(),
410                ))
411            }
412        };
413
414        // Use the signer's sign_xdr_transaction method
415        let response = self
416            .signer
417            .sign_xdr_transaction(&stellar_req.unsigned_xdr, &self.network.passphrase)
418            .await
419            .map_err(RelayerError::SignerError)?;
420
421        // Convert DecoratedSignature to base64 string
422        let signature_bytes = &response.signature.signature.0;
423        let signature_string =
424            base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature_bytes);
425
426        Ok(SignTransactionExternalResponse::Stellar(
427            SignTransactionExternalResponseStellar {
428                signed_xdr: response.signed_xdr,
429                signature: signature_string,
430            },
431        ))
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::{
439        config::{NetworkConfigCommon, StellarNetworkConfig},
440        constants::STELLAR_SMALLEST_UNIT_NAME,
441        domain::{SignTransactionRequestStellar, SignXdrTransactionResponseStellar},
442        jobs::MockJobProducerTrait,
443        models::{
444            NetworkConfigData, NetworkRepoModel, NetworkType, RelayerNetworkPolicy,
445            RelayerRepoModel, RelayerStellarPolicy, SignerError,
446        },
447        repositories::{
448            InMemoryNetworkRepository, MockRelayerRepository, MockTransactionRepository,
449        },
450        services::{
451            MockStellarProviderTrait, MockStellarSignTrait, MockTransactionCounterServiceTrait,
452        },
453    };
454    use eyre::eyre;
455    use mockall::predicate::*;
456    use soroban_rs::xdr::{
457        AccountEntry, AccountEntryExt, AccountId, DecoratedSignature, PublicKey, SequenceNumber,
458        Signature, SignatureHint, String32, Thresholds, Uint256, VecM,
459    };
460    use std::future::ready;
461    use std::sync::Arc;
462
463    /// Test context structure to manage test dependencies
464    struct TestCtx {
465        relayer_model: RelayerRepoModel,
466        network_repository: Arc<InMemoryNetworkRepository>,
467    }
468
469    impl Default for TestCtx {
470        fn default() -> Self {
471            let network_repository = Arc::new(InMemoryNetworkRepository::new());
472
473            let relayer_model = RelayerRepoModel {
474                id: "test-relayer-id".to_string(),
475                name: "Test Relayer".to_string(),
476                network: "testnet".to_string(),
477                paused: false,
478                network_type: NetworkType::Stellar,
479                signer_id: "signer-id".to_string(),
480                policies: RelayerNetworkPolicy::Stellar(RelayerStellarPolicy::default()),
481                address: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
482                notification_id: Some("notification-id".to_string()),
483                system_disabled: false,
484                custom_rpc_urls: None,
485            };
486
487            TestCtx {
488                relayer_model,
489                network_repository,
490            }
491        }
492    }
493
494    impl TestCtx {
495        async fn setup_network(&self) {
496            let test_network = NetworkRepoModel {
497                id: "stellar:testnet".to_string(),
498                name: "testnet".to_string(),
499                network_type: NetworkType::Stellar,
500                config: NetworkConfigData::Stellar(StellarNetworkConfig {
501                    common: NetworkConfigCommon {
502                        network: "testnet".to_string(),
503                        from: None,
504                        rpc_urls: Some(vec!["https://horizon-testnet.stellar.org".to_string()]),
505                        explorer_urls: None,
506                        average_blocktime_ms: Some(5000),
507                        is_testnet: Some(true),
508                        tags: None,
509                    },
510                    passphrase: Some("Test SDF Network ; September 2015".to_string()),
511                }),
512            };
513
514            self.network_repository.create(test_network).await.unwrap();
515        }
516    }
517
518    #[tokio::test]
519    async fn test_sync_sequence_success() {
520        let ctx = TestCtx::default();
521        ctx.setup_network().await;
522        let relayer_model = ctx.relayer_model.clone();
523        let mut provider = MockStellarProviderTrait::new();
524        provider
525            .expect_get_account()
526            .with(eq(relayer_model.address.clone()))
527            .returning(|_| {
528                Box::pin(async {
529                    Ok(AccountEntry {
530                        account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
531                        balance: 0,
532                        ext: AccountEntryExt::V0,
533                        flags: 0,
534                        home_domain: String32::default(),
535                        inflation_dest: None,
536                        seq_num: SequenceNumber(5),
537                        num_sub_entries: 0,
538                        signers: VecM::default(),
539                        thresholds: Thresholds([0, 0, 0, 0]),
540                    })
541                })
542            });
543        let mut counter = MockTransactionCounterServiceTrait::new();
544        counter
545            .expect_set()
546            .with(eq(6u64))
547            .returning(|_| Box::pin(async { Ok(()) }));
548        let relayer_repo = MockRelayerRepository::new();
549        let tx_repo = MockTransactionRepository::new();
550        let job_producer = MockJobProducerTrait::new();
551        let signer = MockStellarSignTrait::new();
552
553        let relayer = StellarRelayer::new(
554            relayer_model.clone(),
555            signer,
556            provider,
557            StellarRelayerDependencies::new(
558                Arc::new(relayer_repo),
559                ctx.network_repository.clone(),
560                Arc::new(tx_repo),
561                Arc::new(counter),
562                Arc::new(job_producer),
563            ),
564        )
565        .await
566        .unwrap();
567
568        let result = relayer.sync_sequence().await;
569        assert!(result.is_ok());
570    }
571
572    #[tokio::test]
573    async fn test_sync_sequence_provider_error() {
574        let ctx = TestCtx::default();
575        ctx.setup_network().await;
576        let relayer_model = ctx.relayer_model.clone();
577        let mut provider = MockStellarProviderTrait::new();
578        provider
579            .expect_get_account()
580            .with(eq(relayer_model.address.clone()))
581            .returning(|_| Box::pin(async { Err(eyre!("fail")) }));
582        let counter = MockTransactionCounterServiceTrait::new();
583        let relayer_repo = MockRelayerRepository::new();
584        let tx_repo = MockTransactionRepository::new();
585        let job_producer = MockJobProducerTrait::new();
586        let signer = MockStellarSignTrait::new();
587
588        let relayer = StellarRelayer::new(
589            relayer_model.clone(),
590            signer,
591            provider,
592            StellarRelayerDependencies::new(
593                Arc::new(relayer_repo),
594                ctx.network_repository.clone(),
595                Arc::new(tx_repo),
596                Arc::new(counter),
597                Arc::new(job_producer),
598            ),
599        )
600        .await
601        .unwrap();
602
603        let result = relayer.sync_sequence().await;
604        assert!(matches!(result, Err(RelayerError::ProviderError(_))));
605    }
606
607    #[tokio::test]
608    async fn test_disable_relayer() {
609        let ctx = TestCtx::default();
610        ctx.setup_network().await;
611        let relayer_model = ctx.relayer_model.clone();
612        let provider = MockStellarProviderTrait::new();
613        let mut relayer_repo = MockRelayerRepository::new();
614        let mut updated_model = relayer_model.clone();
615        updated_model.system_disabled = true;
616        relayer_repo
617            .expect_disable_relayer()
618            .with(eq(relayer_model.id.clone()))
619            .returning(move |_| Ok::<RelayerRepoModel, RepositoryError>(updated_model.clone()));
620        let mut job_producer = MockJobProducerTrait::new();
621        job_producer
622            .expect_produce_send_notification_job()
623            .returning(|_, _| Box::pin(async { Ok(()) }));
624        let tx_repo = MockTransactionRepository::new();
625        let counter = MockTransactionCounterServiceTrait::new();
626        let signer = MockStellarSignTrait::new();
627
628        let relayer = StellarRelayer::new(
629            relayer_model.clone(),
630            signer,
631            provider,
632            StellarRelayerDependencies::new(
633                Arc::new(relayer_repo),
634                ctx.network_repository.clone(),
635                Arc::new(tx_repo),
636                Arc::new(counter),
637                Arc::new(job_producer),
638            ),
639        )
640        .await
641        .unwrap();
642
643        let reasons = vec!["reason1".to_string(), "reason2".to_string()];
644        let result = relayer.disable_relayer(&reasons).await;
645        assert!(result.is_ok());
646    }
647
648    #[tokio::test]
649    async fn test_get_status_success_stellar() {
650        let ctx = TestCtx::default();
651        ctx.setup_network().await;
652        let relayer_model = ctx.relayer_model.clone();
653        let mut provider_mock = MockStellarProviderTrait::new();
654        let mut tx_repo_mock = MockTransactionRepository::new();
655        let relayer_repo_mock = MockRelayerRepository::new();
656        let job_producer_mock = MockJobProducerTrait::new();
657        let counter_mock = MockTransactionCounterServiceTrait::new();
658
659        provider_mock.expect_get_account().times(2).returning(|_| {
660            Box::pin(ready(Ok(AccountEntry {
661                account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
662                balance: 10000000,
663                seq_num: SequenceNumber(12345),
664                ext: AccountEntryExt::V0,
665                flags: 0,
666                home_domain: String32::default(),
667                inflation_dest: None,
668                num_sub_entries: 0,
669                signers: VecM::default(),
670                thresholds: Thresholds([0, 0, 0, 0]),
671            })))
672        });
673
674        tx_repo_mock
675            .expect_find_by_status()
676            .withf(|relayer_id, statuses| {
677                relayer_id == "test-relayer-id"
678                    && statuses == [TransactionStatus::Pending, TransactionStatus::Submitted]
679            })
680            .returning(|_, _| Ok(vec![]) as Result<Vec<TransactionRepoModel>, RepositoryError>)
681            .once();
682
683        let confirmed_tx = TransactionRepoModel {
684            id: "tx1_stellar".to_string(),
685            relayer_id: relayer_model.id.clone(),
686            status: TransactionStatus::Confirmed,
687            confirmed_at: Some("2023-02-01T12:00:00Z".to_string()),
688            ..TransactionRepoModel::default()
689        };
690        tx_repo_mock
691            .expect_find_by_status()
692            .withf(|relayer_id, statuses| {
693                relayer_id == "test-relayer-id" && statuses == [TransactionStatus::Confirmed]
694            })
695            .returning(move |_, _| {
696                Ok(vec![confirmed_tx.clone()]) as Result<Vec<TransactionRepoModel>, RepositoryError>
697            })
698            .once();
699        let signer = MockStellarSignTrait::new();
700
701        let stellar_relayer = StellarRelayer::new(
702            relayer_model.clone(),
703            signer,
704            provider_mock,
705            StellarRelayerDependencies::new(
706                Arc::new(relayer_repo_mock),
707                ctx.network_repository.clone(),
708                Arc::new(tx_repo_mock),
709                Arc::new(counter_mock),
710                Arc::new(job_producer_mock),
711            ),
712        )
713        .await
714        .unwrap();
715
716        let status = stellar_relayer.get_status().await.unwrap();
717
718        match status {
719            RelayerStatus::Stellar {
720                balance,
721                pending_transactions_count,
722                last_confirmed_transaction_timestamp,
723                system_disabled,
724                paused,
725                sequence_number,
726            } => {
727                assert_eq!(balance, "10000000");
728                assert_eq!(pending_transactions_count, 0);
729                assert_eq!(
730                    last_confirmed_transaction_timestamp,
731                    Some("2023-02-01T12:00:00Z".to_string())
732                );
733                assert_eq!(system_disabled, relayer_model.system_disabled);
734                assert_eq!(paused, relayer_model.paused);
735                assert_eq!(sequence_number, "12345");
736            }
737            _ => panic!("Expected Stellar RelayerStatus"),
738        }
739    }
740
741    #[tokio::test]
742    async fn test_get_status_stellar_provider_error() {
743        let ctx = TestCtx::default();
744        ctx.setup_network().await;
745        let relayer_model = ctx.relayer_model.clone();
746        let mut provider_mock = MockStellarProviderTrait::new();
747        let tx_repo_mock = MockTransactionRepository::new();
748        let relayer_repo_mock = MockRelayerRepository::new();
749        let job_producer_mock = MockJobProducerTrait::new();
750        let counter_mock = MockTransactionCounterServiceTrait::new();
751
752        provider_mock
753            .expect_get_account()
754            .with(eq(relayer_model.address.clone()))
755            .returning(|_| Box::pin(async { Err(eyre!("Stellar provider down")) }));
756        let signer = MockStellarSignTrait::new();
757
758        let stellar_relayer = StellarRelayer::new(
759            relayer_model.clone(),
760            signer,
761            provider_mock,
762            StellarRelayerDependencies::new(
763                Arc::new(relayer_repo_mock),
764                ctx.network_repository.clone(),
765                Arc::new(tx_repo_mock),
766                Arc::new(counter_mock),
767                Arc::new(job_producer_mock),
768            ),
769        )
770        .await
771        .unwrap();
772
773        let result = stellar_relayer.get_status().await;
774        assert!(result.is_err());
775        match result.err().unwrap() {
776            RelayerError::ProviderError(msg) => {
777                assert!(msg.contains("Failed to get account details"))
778            }
779            _ => panic!("Expected ProviderError for get_account failure"),
780        }
781    }
782
783    #[tokio::test]
784    async fn test_get_balance_success() {
785        let ctx = TestCtx::default();
786        ctx.setup_network().await;
787        let relayer_model = ctx.relayer_model.clone();
788        let mut provider = MockStellarProviderTrait::new();
789        let expected_balance = 100_000_000i64; // 10 XLM in stroops
790
791        provider
792            .expect_get_account()
793            .with(eq(relayer_model.address.clone()))
794            .returning(move |_| {
795                Box::pin(async move {
796                    Ok(AccountEntry {
797                        account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
798                        balance: expected_balance,
799                        ext: AccountEntryExt::V0,
800                        flags: 0,
801                        home_domain: String32::default(),
802                        inflation_dest: None,
803                        seq_num: SequenceNumber(5),
804                        num_sub_entries: 0,
805                        signers: VecM::default(),
806                        thresholds: Thresholds([0, 0, 0, 0]),
807                    })
808                })
809            });
810
811        let relayer_repo = Arc::new(MockRelayerRepository::new());
812        let tx_repo = Arc::new(MockTransactionRepository::new());
813        let job_producer = Arc::new(MockJobProducerTrait::new());
814        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
815        let signer = MockStellarSignTrait::new();
816
817        let relayer = StellarRelayer::new(
818            relayer_model,
819            signer,
820            provider,
821            StellarRelayerDependencies::new(
822                relayer_repo,
823                ctx.network_repository.clone(),
824                tx_repo,
825                counter,
826                job_producer,
827            ),
828        )
829        .await
830        .unwrap();
831
832        let result = relayer.get_balance().await;
833        assert!(result.is_ok());
834        let balance_response = result.unwrap();
835        assert_eq!(balance_response.balance, expected_balance as u128);
836        assert_eq!(balance_response.unit, STELLAR_SMALLEST_UNIT_NAME);
837    }
838
839    #[tokio::test]
840    async fn test_get_balance_provider_error() {
841        let ctx = TestCtx::default();
842        ctx.setup_network().await;
843        let relayer_model = ctx.relayer_model.clone();
844        let mut provider = MockStellarProviderTrait::new();
845
846        provider
847            .expect_get_account()
848            .with(eq(relayer_model.address.clone()))
849            .returning(|_| Box::pin(async { Err(eyre!("provider failed")) }));
850
851        let relayer_repo = Arc::new(MockRelayerRepository::new());
852        let tx_repo = Arc::new(MockTransactionRepository::new());
853        let job_producer = Arc::new(MockJobProducerTrait::new());
854        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
855        let signer = MockStellarSignTrait::new();
856
857        let relayer = StellarRelayer::new(
858            relayer_model,
859            signer,
860            provider,
861            StellarRelayerDependencies::new(
862                relayer_repo,
863                ctx.network_repository.clone(),
864                tx_repo,
865                counter,
866                job_producer,
867            ),
868        )
869        .await
870        .unwrap();
871
872        let result = relayer.get_balance().await;
873        assert!(result.is_err());
874        match result.err().unwrap() {
875            RelayerError::ProviderError(msg) => {
876                assert!(msg.contains("Failed to fetch account for balance: provider failed"));
877            }
878            _ => panic!("Unexpected error type"),
879        }
880    }
881
882    #[tokio::test]
883    async fn test_sign_transaction_success() {
884        let ctx = TestCtx::default();
885        ctx.setup_network().await;
886        let relayer_model = ctx.relayer_model.clone();
887        let provider = MockStellarProviderTrait::new();
888        let mut signer = MockStellarSignTrait::new();
889
890        let unsigned_xdr = "AAAAAgAAAAD///8AAAAAAAAAAQAAAAAAAAACAAAAAQAAAAAAAAAB";
891        let expected_signed_xdr =
892            "AAAAAgAAAAD///8AAAAAAAABAAAAAAAAAAIAAAABAAAAAAAAAAEAAAABAAAAA...";
893        let expected_signature = DecoratedSignature {
894            hint: SignatureHint([1, 2, 3, 4]),
895            signature: Signature([5u8; 64].try_into().unwrap()),
896        };
897        let expected_signature_for_closure = expected_signature.clone();
898
899        signer
900            .expect_sign_xdr_transaction()
901            .with(eq(unsigned_xdr), eq("Test SDF Network ; September 2015"))
902            .returning(move |_, _| {
903                Ok(SignXdrTransactionResponseStellar {
904                    signed_xdr: expected_signed_xdr.to_string(),
905                    signature: expected_signature_for_closure.clone(),
906                })
907            });
908
909        let relayer_repo = Arc::new(MockRelayerRepository::new());
910        let tx_repo = Arc::new(MockTransactionRepository::new());
911        let job_producer = Arc::new(MockJobProducerTrait::new());
912        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
913
914        let relayer = StellarRelayer::new(
915            relayer_model,
916            signer,
917            provider,
918            StellarRelayerDependencies::new(
919                relayer_repo,
920                ctx.network_repository.clone(),
921                tx_repo,
922                counter,
923                job_producer,
924            ),
925        )
926        .await
927        .unwrap();
928
929        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
930            unsigned_xdr: unsigned_xdr.to_string(),
931        });
932        let result = relayer.sign_transaction(&request).await;
933        assert!(result.is_ok());
934
935        match result.unwrap() {
936            SignTransactionExternalResponse::Stellar(response) => {
937                assert_eq!(response.signed_xdr, expected_signed_xdr);
938                // Compare the base64 encoded signature
939                let expected_signature_base64 = base64::Engine::encode(
940                    &base64::engine::general_purpose::STANDARD,
941                    &expected_signature.signature.0,
942                );
943                assert_eq!(response.signature, expected_signature_base64);
944            }
945            _ => panic!("Expected Stellar response"),
946        }
947    }
948
949    #[tokio::test]
950    async fn test_sign_transaction_signer_error() {
951        let ctx = TestCtx::default();
952        ctx.setup_network().await;
953        let relayer_model = ctx.relayer_model.clone();
954        let provider = MockStellarProviderTrait::new();
955        let mut signer = MockStellarSignTrait::new();
956
957        let unsigned_xdr = "INVALID_XDR";
958
959        signer
960            .expect_sign_xdr_transaction()
961            .with(eq(unsigned_xdr), eq("Test SDF Network ; September 2015"))
962            .returning(|_, _| Err(SignerError::SigningError("Invalid XDR format".to_string())));
963
964        let relayer_repo = Arc::new(MockRelayerRepository::new());
965        let tx_repo = Arc::new(MockTransactionRepository::new());
966        let job_producer = Arc::new(MockJobProducerTrait::new());
967        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
968
969        let relayer = StellarRelayer::new(
970            relayer_model,
971            signer,
972            provider,
973            StellarRelayerDependencies::new(
974                relayer_repo,
975                ctx.network_repository.clone(),
976                tx_repo,
977                counter,
978                job_producer,
979            ),
980        )
981        .await
982        .unwrap();
983
984        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
985            unsigned_xdr: unsigned_xdr.to_string(),
986        });
987        let result = relayer.sign_transaction(&request).await;
988        assert!(result.is_err());
989
990        match result.err().unwrap() {
991            RelayerError::SignerError(err) => match err {
992                SignerError::SigningError(msg) => {
993                    assert_eq!(msg, "Invalid XDR format");
994                }
995                _ => panic!("Expected SigningError"),
996            },
997            _ => panic!("Expected RelayerError::SignerError"),
998        }
999    }
1000
1001    #[tokio::test]
1002    async fn test_sign_transaction_with_different_network_passphrase() {
1003        let ctx = TestCtx::default();
1004        // Create a custom network with a different passphrase
1005        let custom_network = NetworkRepoModel {
1006            id: "stellar:mainnet".to_string(),
1007            name: "mainnet".to_string(),
1008            network_type: NetworkType::Stellar,
1009            config: NetworkConfigData::Stellar(StellarNetworkConfig {
1010                common: NetworkConfigCommon {
1011                    network: "mainnet".to_string(),
1012                    from: None,
1013                    rpc_urls: Some(vec!["https://horizon.stellar.org".to_string()]),
1014                    explorer_urls: None,
1015                    average_blocktime_ms: Some(5000),
1016                    is_testnet: Some(false),
1017                    tags: None,
1018                },
1019                passphrase: Some("Public Global Stellar Network ; September 2015".to_string()),
1020            }),
1021        };
1022        ctx.network_repository.create(custom_network).await.unwrap();
1023
1024        let mut relayer_model = ctx.relayer_model.clone();
1025        relayer_model.network = "mainnet".to_string();
1026
1027        let provider = MockStellarProviderTrait::new();
1028        let mut signer = MockStellarSignTrait::new();
1029
1030        let unsigned_xdr = "AAAAAgAAAAD///8AAAAAAAAAAQAAAAAAAAACAAAAAQAAAAAAAAAB";
1031        let expected_signature = DecoratedSignature {
1032            hint: SignatureHint([10, 20, 30, 40]),
1033            signature: Signature([15u8; 64].try_into().unwrap()),
1034        };
1035        let expected_signature_for_closure = expected_signature.clone();
1036
1037        signer
1038            .expect_sign_xdr_transaction()
1039            .with(
1040                eq(unsigned_xdr),
1041                eq("Public Global Stellar Network ; September 2015"),
1042            )
1043            .returning(move |_, _| {
1044                Ok(SignXdrTransactionResponseStellar {
1045                    signed_xdr: "mainnet_signed_xdr".to_string(),
1046                    signature: expected_signature_for_closure.clone(),
1047                })
1048            });
1049
1050        let relayer_repo = Arc::new(MockRelayerRepository::new());
1051        let tx_repo = Arc::new(MockTransactionRepository::new());
1052        let job_producer = Arc::new(MockJobProducerTrait::new());
1053        let counter = Arc::new(MockTransactionCounterServiceTrait::new());
1054
1055        let relayer = StellarRelayer::new(
1056            relayer_model,
1057            signer,
1058            provider,
1059            StellarRelayerDependencies::new(
1060                relayer_repo,
1061                ctx.network_repository.clone(),
1062                tx_repo,
1063                counter,
1064                job_producer,
1065            ),
1066        )
1067        .await
1068        .unwrap();
1069
1070        let request = SignTransactionRequest::Stellar(SignTransactionRequestStellar {
1071            unsigned_xdr: unsigned_xdr.to_string(),
1072        });
1073        let result = relayer.sign_transaction(&request).await;
1074        assert!(result.is_ok());
1075
1076        match result.unwrap() {
1077            SignTransactionExternalResponse::Stellar(response) => {
1078                assert_eq!(response.signed_xdr, "mainnet_signed_xdr");
1079                // Convert expected signature to base64 for comparison (just the signature bytes, not the whole struct)
1080                let expected_signature_string = base64::Engine::encode(
1081                    &base64::engine::general_purpose::STANDARD,
1082                    &expected_signature.signature.0,
1083                );
1084                assert_eq!(response.signature, expected_signature_string);
1085            }
1086            _ => panic!("Expected Stellar response"),
1087        }
1088    }
1089}