openzeppelin_relayer/domain/transaction/
mod.rs

1//! This module defines the core transaction handling logic for different blockchain networks,
2//! including Ethereum (EVM), Solana, and Stellar. It provides a unified interface for preparing,
3//! submitting, handling, canceling, replacing, signing, and validating transactions across these
4//! networks. The module also includes a factory for creating network-specific transaction handlers
5//! based on relayer and repository information.
6//!
7//! The main components of this module are:
8//! - `Transaction` trait: Defines the operations for handling transactions.
9//! - `NetworkTransaction` enum: Represents a transaction for different network types.
10//! - `RelayerTransactionFactory`: A factory for creating network transactions.
11//!
12//! The module leverages async traits to handle asynchronous operations and uses the `eyre` crate
13//! for error handling.
14use crate::{
15    constants::{STELLAR_HORIZON_MAINNET_URL, STELLAR_HORIZON_TESTNET_URL},
16    jobs::{JobProducer, StatusCheckContext},
17    models::{
18        EvmNetwork, NetworkTransactionRequest, NetworkType, RelayerRepoModel, SignerRepoModel,
19        SolanaNetwork, StellarNetwork, StellarSwapStrategy, TransactionError, TransactionRepoModel,
20    },
21    repositories::{
22        NetworkRepository, NetworkRepositoryStorage, RelayerRepositoryStorage,
23        TransactionCounterRepositoryStorage, TransactionRepositoryStorage,
24    },
25    services::{
26        gas::{
27            cache::GasPriceCache, evm_gas_price::EvmGasPriceService,
28            price_params_handler::PriceParamsHandler,
29        },
30        provider::get_network_provider,
31        signer::{EvmSignerFactory, SolanaSignerFactory, StellarSignerFactory},
32        stellar_dex::{DexServiceWrapper, OrderBookService, SoroswapService, StellarDexService},
33    },
34};
35use async_trait::async_trait;
36use eyre::Result;
37#[cfg(test)]
38use mockall::automock;
39use std::sync::Arc;
40use tracing::instrument;
41
42pub mod common;
43pub mod evm;
44pub mod solana;
45pub mod stellar;
46
47mod util;
48pub use util::*;
49
50// Explicit re-exports to avoid ambiguous glob re-exports
51pub use common::is_final_state;
52pub use common::*;
53pub use evm::{ensure_status, ensure_status_one_of, DefaultEvmTransaction, EvmRelayerTransaction};
54pub use solana::{DefaultSolanaTransaction, SolanaRelayerTransaction};
55pub use stellar::{DefaultStellarTransaction, StellarRelayerTransaction};
56
57/// A trait that defines the operations for handling transactions across different networks.
58#[cfg_attr(test, automock)]
59#[async_trait]
60#[allow(dead_code)]
61pub trait Transaction {
62    /// Prepares a transaction for submission.
63    ///
64    /// # Arguments
65    ///
66    /// * `tx` - A `TransactionRepoModel` representing the transaction to be prepared.
67    ///
68    /// # Returns
69    ///
70    /// A `Result` containing the prepared `TransactionRepoModel` or a `TransactionError`.
71    async fn prepare_transaction(
72        &self,
73        tx: TransactionRepoModel,
74    ) -> Result<TransactionRepoModel, TransactionError>;
75
76    /// Submits a transaction to the network.
77    ///
78    /// # Arguments
79    ///
80    /// * `tx` - A `TransactionRepoModel` representing the transaction to be submitted.
81    ///
82    /// # Returns
83    ///
84    /// A `Result` containing the submitted `TransactionRepoModel` or a `TransactionError`.
85    async fn submit_transaction(
86        &self,
87        tx: TransactionRepoModel,
88    ) -> Result<TransactionRepoModel, TransactionError>;
89
90    /// Resubmits a transaction with updated parameters.
91    ///
92    /// # Arguments
93    ///
94    /// * `tx` - A `TransactionRepoModel` representing the transaction to be resubmitted.
95    ///
96    /// # Returns
97    ///
98    /// A `Result` containing the resubmitted `TransactionRepoModel` or a `TransactionError`.
99    async fn resubmit_transaction(
100        &self,
101        tx: TransactionRepoModel,
102    ) -> Result<TransactionRepoModel, TransactionError>;
103
104    /// Handles the status of a transaction.
105    ///
106    /// # Arguments
107    ///
108    /// * `tx` - A `TransactionRepoModel` representing the transaction whose status is to be
109    ///   handled.
110    /// * `context` - Optional `StatusCheckContext` containing failure tracking information
111    ///   for circuit breaker decisions. When provided, handlers can use this to decide
112    ///   whether to force-finalize a transaction that has exceeded retry limits.
113    ///
114    /// # Returns
115    ///
116    /// A `Result` containing the updated `TransactionRepoModel` or a `TransactionError`.
117    async fn handle_transaction_status(
118        &self,
119        tx: TransactionRepoModel,
120        context: Option<StatusCheckContext>,
121    ) -> Result<TransactionRepoModel, TransactionError>;
122
123    /// Cancels a transaction.
124    ///
125    /// # Arguments
126    ///
127    /// * `tx` - A `TransactionRepoModel` representing the transaction to be canceled.
128    ///
129    /// # Returns
130    ///
131    /// A `Result` containing the canceled `TransactionRepoModel` or a `TransactionError`.
132    async fn cancel_transaction(
133        &self,
134        tx: TransactionRepoModel,
135    ) -> Result<TransactionRepoModel, TransactionError>;
136
137    /// Replaces a transaction with a new one.
138    ///
139    /// # Arguments
140    ///
141    /// * `old_tx` - A `TransactionRepoModel` representing the transaction to be replaced.
142    /// * `new_tx_request` - A `NetworkTransactionRequest` representing the new transaction data.
143    ///
144    /// # Returns
145    ///
146    /// A `Result` containing the new `TransactionRepoModel` or a `TransactionError`.
147    async fn replace_transaction(
148        &self,
149        old_tx: TransactionRepoModel,
150        new_tx_request: NetworkTransactionRequest,
151    ) -> Result<TransactionRepoModel, TransactionError>;
152
153    /// Signs a transaction.
154    ///
155    /// # Arguments
156    ///
157    /// * `tx` - A `TransactionRepoModel` representing the transaction to be signed.
158    ///
159    /// # Returns
160    ///
161    /// A `Result` containing the signed `TransactionRepoModel` or a `TransactionError`.
162    async fn sign_transaction(
163        &self,
164        tx: TransactionRepoModel,
165    ) -> Result<TransactionRepoModel, TransactionError>;
166
167    /// Validates a transaction.
168    ///
169    /// # Arguments
170    ///
171    /// * `tx` - A `TransactionRepoModel` representing the transaction to be validated.
172    ///
173    /// # Returns
174    ///
175    /// A `Result` containing a boolean indicating the validity of the transaction or a
176    /// `TransactionError`.
177    async fn validate_transaction(
178        &self,
179        tx: TransactionRepoModel,
180    ) -> Result<bool, TransactionError>;
181}
182
183/// An enum representing a transaction for different network types.
184pub enum NetworkTransaction {
185    Evm(Box<DefaultEvmTransaction>),
186    Solana(DefaultSolanaTransaction),
187    Stellar(DefaultStellarTransaction),
188}
189
190#[async_trait]
191impl Transaction for NetworkTransaction {
192    /// Prepares a transaction for submission based on the network type.
193    ///
194    /// # Arguments
195    ///
196    /// * `tx` - A `TransactionRepoModel` representing the transaction to be prepared.
197    ///
198    /// # Returns
199    ///
200    /// A `Result` containing the prepared `TransactionRepoModel` or a `TransactionError`.
201    #[instrument(
202        level = "debug",
203        skip(self, tx),
204        fields(
205            request_id = ?crate::observability::request_id::get_request_id(),
206            tx_id = %tx.id,
207            relayer_id = %tx.relayer_id,
208            tx_status = ?tx.status,
209            network_type = ?tx.network_type,
210        )
211    )]
212    async fn prepare_transaction(
213        &self,
214        tx: TransactionRepoModel,
215    ) -> Result<TransactionRepoModel, TransactionError> {
216        match self {
217            NetworkTransaction::Evm(relayer) => relayer.prepare_transaction(tx).await,
218            NetworkTransaction::Solana(relayer) => relayer.prepare_transaction(tx).await,
219            NetworkTransaction::Stellar(relayer) => relayer.prepare_transaction(tx).await,
220        }
221    }
222
223    /// Submits a transaction to the network based on the network type.
224    ///
225    /// # Arguments
226    ///
227    /// * `tx` - A `TransactionRepoModel` representing the transaction to be submitted.
228    ///
229    /// # Returns
230    ///
231    /// A `Result` containing the submitted `TransactionRepoModel` or a `TransactionError`.
232    #[instrument(
233        level = "debug",
234        skip(self, tx),
235        fields(
236            request_id = ?crate::observability::request_id::get_request_id(),
237            tx_id = %tx.id,
238            relayer_id = %tx.relayer_id,
239            tx_status = ?tx.status,
240            network_type = ?tx.network_type,
241        )
242    )]
243    async fn submit_transaction(
244        &self,
245        tx: TransactionRepoModel,
246    ) -> Result<TransactionRepoModel, TransactionError> {
247        match self {
248            NetworkTransaction::Evm(relayer) => relayer.submit_transaction(tx).await,
249            NetworkTransaction::Solana(relayer) => relayer.submit_transaction(tx).await,
250            NetworkTransaction::Stellar(relayer) => relayer.submit_transaction(tx).await,
251        }
252    }
253    /// Resubmits a transaction with updated parameters based on the network type.
254    ///
255    /// # Arguments
256    ///
257    /// * `tx` - A `TransactionRepoModel` representing the transaction to be resubmitted.
258    ///
259    /// # Returns
260    ///
261    /// A `Result` containing the resubmitted `TransactionRepoModel` or a `TransactionError`.
262    #[instrument(
263        level = "debug",
264        skip(self, tx),
265        fields(
266            request_id = ?crate::observability::request_id::get_request_id(),
267            tx_id = %tx.id,
268            relayer_id = %tx.relayer_id,
269            tx_status = ?tx.status,
270            network_type = ?tx.network_type,
271        )
272    )]
273    async fn resubmit_transaction(
274        &self,
275        tx: TransactionRepoModel,
276    ) -> Result<TransactionRepoModel, TransactionError> {
277        match self {
278            NetworkTransaction::Evm(relayer) => relayer.resubmit_transaction(tx).await,
279            NetworkTransaction::Solana(relayer) => relayer.resubmit_transaction(tx).await,
280            NetworkTransaction::Stellar(relayer) => relayer.resubmit_transaction(tx).await,
281        }
282    }
283
284    /// Handles the status of a transaction based on the network type.
285    ///
286    /// # Arguments
287    ///
288    /// * `tx` - A `TransactionRepoModel` representing the transaction whose status is to be
289    ///   handled.
290    /// * `context` - Optional `StatusCheckContext` containing failure tracking information
291    ///   for circuit breaker decisions.
292    ///
293    /// # Returns
294    ///
295    /// A `Result` containing the updated `TransactionRepoModel` or a `TransactionError`.
296    #[instrument(
297        level = "debug",
298        skip(self, tx, context),
299        fields(
300            request_id = ?crate::observability::request_id::get_request_id(),
301            tx_id = %tx.id,
302            relayer_id = %tx.relayer_id,
303            tx_status = ?tx.status,
304            network_type = ?tx.network_type,
305            has_context = %context.is_some(),
306        )
307    )]
308    async fn handle_transaction_status(
309        &self,
310        tx: TransactionRepoModel,
311        context: Option<StatusCheckContext>,
312    ) -> Result<TransactionRepoModel, TransactionError> {
313        match self {
314            NetworkTransaction::Evm(relayer) => {
315                relayer.handle_transaction_status(tx, context).await
316            }
317            NetworkTransaction::Solana(relayer) => {
318                relayer.handle_transaction_status(tx, context).await
319            }
320            NetworkTransaction::Stellar(relayer) => {
321                relayer.handle_transaction_status(tx, context).await
322            }
323        }
324    }
325
326    /// Cancels a transaction based on the network type.
327    ///
328    /// # Arguments
329    ///
330    /// * `tx` - A `TransactionRepoModel` representing the transaction to be canceled.
331    ///
332    /// # Returns
333    ///
334    /// A `Result` containing the canceled `TransactionRepoModel` or a `TransactionError`.
335    #[instrument(
336        level = "debug",
337        skip(self, tx),
338        fields(
339            request_id = ?crate::observability::request_id::get_request_id(),
340            tx_id = %tx.id,
341            relayer_id = %tx.relayer_id,
342            tx_status = ?tx.status,
343            network_type = ?tx.network_type,
344        )
345    )]
346    async fn cancel_transaction(
347        &self,
348        tx: TransactionRepoModel,
349    ) -> Result<TransactionRepoModel, TransactionError> {
350        match self {
351            NetworkTransaction::Evm(relayer) => relayer.cancel_transaction(tx).await,
352            NetworkTransaction::Solana(_) => solana_not_supported_transaction(),
353            NetworkTransaction::Stellar(relayer) => relayer.cancel_transaction(tx).await,
354        }
355    }
356
357    /// Replaces a transaction with a new one based on the network type.
358    ///
359    /// # Arguments
360    ///
361    /// * `old_tx` - A `TransactionRepoModel` representing the transaction to be replaced.
362    /// * `new_tx_request` - A `NetworkTransactionRequest` representing the new transaction data.
363    ///
364    /// # Returns
365    ///
366    /// A `Result` containing the new `TransactionRepoModel` or a `TransactionError`.
367    #[instrument(
368        level = "debug",
369        skip(self, old_tx, new_tx_request),
370        fields(
371            request_id = ?crate::observability::request_id::get_request_id(),
372            tx_id = %old_tx.id,
373            relayer_id = %old_tx.relayer_id,
374            tx_status = ?old_tx.status,
375            network_type = ?old_tx.network_type,
376        )
377    )]
378    async fn replace_transaction(
379        &self,
380        old_tx: TransactionRepoModel,
381        new_tx_request: NetworkTransactionRequest,
382    ) -> Result<TransactionRepoModel, TransactionError> {
383        match self {
384            NetworkTransaction::Evm(relayer) => {
385                relayer.replace_transaction(old_tx, new_tx_request).await
386            }
387            NetworkTransaction::Solana(_) => solana_not_supported_transaction(),
388            NetworkTransaction::Stellar(relayer) => {
389                relayer.replace_transaction(old_tx, new_tx_request).await
390            }
391        }
392    }
393
394    /// Signs a transaction based on the network type.
395    ///
396    /// # Arguments
397    ///
398    /// * `tx` - A `TransactionRepoModel` representing the transaction to be signed.
399    ///
400    /// # Returns
401    ///
402    /// A `Result` containing the signed `TransactionRepoModel` or a `TransactionError`.
403    #[instrument(
404        level = "debug",
405        skip(self, tx),
406        fields(
407            request_id = ?crate::observability::request_id::get_request_id(),
408            tx_id = %tx.id,
409            relayer_id = %tx.relayer_id,
410            tx_status = ?tx.status,
411            network_type = ?tx.network_type,
412        )
413    )]
414    async fn sign_transaction(
415        &self,
416        tx: TransactionRepoModel,
417    ) -> Result<TransactionRepoModel, TransactionError> {
418        match self {
419            NetworkTransaction::Evm(relayer) => relayer.sign_transaction(tx).await,
420            NetworkTransaction::Solana(relayer) => relayer.sign_transaction(tx).await,
421            NetworkTransaction::Stellar(relayer) => relayer.sign_transaction(tx).await,
422        }
423    }
424
425    /// Validates a transaction based on the network type.
426    ///
427    /// # Arguments
428    ///
429    /// * `tx` - A `TransactionRepoModel` representing the transaction to be validated.
430    ///
431    /// # Returns
432    ///
433    /// A `Result` containing a boolean indicating the validity of the transaction or a
434    /// `TransactionError`.
435    #[instrument(
436        level = "debug",
437        skip(self, tx),
438        fields(
439            request_id = ?crate::observability::request_id::get_request_id(),
440            tx_id = %tx.id,
441            relayer_id = %tx.relayer_id,
442            tx_status = ?tx.status,
443            network_type = ?tx.network_type,
444        )
445    )]
446    async fn validate_transaction(
447        &self,
448        tx: TransactionRepoModel,
449    ) -> Result<bool, TransactionError> {
450        match self {
451            NetworkTransaction::Evm(relayer) => relayer.validate_transaction(tx).await,
452            NetworkTransaction::Solana(relayer) => relayer.validate_transaction(tx).await,
453            NetworkTransaction::Stellar(relayer) => relayer.validate_transaction(tx).await,
454        }
455    }
456}
457
458/// A trait for creating network transactions.
459#[allow(dead_code)]
460pub trait RelayerTransactionFactoryTrait {
461    /// Creates a network transaction based on the relayer and repository information.
462    ///
463    /// # Arguments
464    ///
465    /// * `relayer` - A `RelayerRepoModel` representing the relayer.
466    /// * `relayer_repository` - An `Arc` to the `RelayerRepositoryStorage`.
467    /// * `transaction_repository` - An `Arc` to the `TransactionRepositoryStorage`.
468    /// * `job_producer` - An `Arc` to the `JobProducer`.
469    ///
470    /// # Returns
471    ///
472    /// A `Result` containing the created `NetworkTransaction` or a `TransactionError`.
473    fn create_transaction(
474        relayer: RelayerRepoModel,
475        relayer_repository: Arc<RelayerRepositoryStorage>,
476        transaction_repository: Arc<TransactionRepositoryStorage>,
477        job_producer: Arc<JobProducer>,
478    ) -> Result<NetworkTransaction, TransactionError>;
479}
480/// A factory for creating relayer transactions.
481pub struct RelayerTransactionFactory;
482
483#[allow(dead_code)]
484impl RelayerTransactionFactory {
485    /// Creates a network transaction based on the relayer, signer, and repository information.
486    ///
487    /// # Arguments
488    ///
489    /// * `relayer` - A `RelayerRepoModel` representing the relayer.
490    /// * `signer` - A `SignerRepoModel` representing the signer.
491    /// * `relayer_repository` - An `Arc` to the `RelayerRepositoryStorage`.
492    /// * `transaction_repository` - An `Arc` to the `InMemoryTransactionRepository`.
493    /// * `transaction_counter_store` - An `Arc` to the `InMemoryTransactionCounter`.
494    /// * `job_producer` - An `Arc` to the `JobProducer`.
495    ///
496    /// # Returns
497    ///
498    /// A `Result` containing the created `NetworkTransaction` or a `TransactionError`.
499    #[instrument(
500        level = "debug",
501        skip(
502            relayer,
503            signer,
504            relayer_repository,
505            network_repository,
506            transaction_repository,
507            transaction_counter_store,
508            job_producer
509        ),
510        fields(
511            request_id = ?crate::observability::request_id::get_request_id(),
512            relayer_id = %relayer.id,
513            network_type = ?relayer.network_type,
514        )
515    )]
516    pub async fn create_transaction(
517        relayer: RelayerRepoModel,
518        signer: SignerRepoModel,
519        relayer_repository: Arc<RelayerRepositoryStorage>,
520        network_repository: Arc<NetworkRepositoryStorage>,
521        transaction_repository: Arc<TransactionRepositoryStorage>,
522        transaction_counter_store: Arc<TransactionCounterRepositoryStorage>,
523        job_producer: Arc<JobProducer>,
524    ) -> Result<NetworkTransaction, TransactionError> {
525        match relayer.network_type {
526            NetworkType::Evm => {
527                let network_repo = network_repository
528                    .get_by_name(NetworkType::Evm, &relayer.network)
529                    .await
530                    .ok()
531                    .flatten()
532                    .ok_or_else(|| {
533                        TransactionError::NetworkConfiguration(format!(
534                            "Network {} not found",
535                            relayer.network
536                        ))
537                    })?;
538
539                let network = EvmNetwork::try_from(network_repo)
540                    .map_err(|e| TransactionError::NetworkConfiguration(e.to_string()))?;
541
542                let evm_provider = get_network_provider(&network, relayer.custom_rpc_urls.clone())?;
543                let signer_service = EvmSignerFactory::create_evm_signer(signer.into()).await?;
544                let price_params_handler =
545                    PriceParamsHandler::for_network(&network, evm_provider.clone());
546
547                let evm_gas_cache = GasPriceCache::global();
548
549                // Use the global cache if gas price caching is enabled
550                let cache = if let Some(cfg) = &network.gas_price_cache {
551                    evm_gas_cache.configure_network(network.chain_id, cfg.clone());
552                    Some(evm_gas_cache.clone())
553                } else {
554                    if evm_gas_cache.has_configuration_for_network(network.chain_id) {
555                        evm_gas_cache.remove_network(network.chain_id);
556                    }
557                    None
558                };
559
560                let gas_price_service =
561                    EvmGasPriceService::new(evm_provider.clone(), network.clone(), cache);
562
563                let price_calculator =
564                    evm::PriceCalculator::new(gas_price_service, price_params_handler);
565
566                Ok(NetworkTransaction::Evm(Box::new(
567                    DefaultEvmTransaction::new(
568                        relayer,
569                        evm_provider,
570                        relayer_repository,
571                        network_repository,
572                        transaction_repository,
573                        transaction_counter_store,
574                        job_producer,
575                        price_calculator,
576                        signer_service,
577                    )?,
578                )))
579            }
580            NetworkType::Solana => {
581                let network_repo = network_repository
582                    .get_by_name(NetworkType::Solana, &relayer.network)
583                    .await
584                    .ok()
585                    .flatten()
586                    .ok_or_else(|| {
587                        TransactionError::NetworkConfiguration(format!(
588                            "Network {} not found",
589                            relayer.network
590                        ))
591                    })?;
592
593                let network = SolanaNetwork::try_from(network_repo)
594                    .map_err(|e| TransactionError::NetworkConfiguration(e.to_string()))?;
595
596                let solana_provider = Arc::new(get_network_provider(
597                    &network,
598                    relayer.custom_rpc_urls.clone(),
599                )?);
600
601                let signer_service =
602                    Arc::new(SolanaSignerFactory::create_solana_signer(&signer.into())?);
603
604                Ok(NetworkTransaction::Solana(SolanaRelayerTransaction::new(
605                    relayer,
606                    relayer_repository,
607                    solana_provider,
608                    transaction_repository,
609                    job_producer,
610                    signer_service,
611                )?))
612            }
613            NetworkType::Stellar => {
614                // Create signer once and wrap in Arc, then clone Arc for both uses
615                // Arc implements Clone (cheap reference count increment)
616                let stellar_signer = StellarSignerFactory::create_stellar_signer(&signer.into())?;
617                let signer_service = Arc::new(stellar_signer);
618
619                let network_repo = network_repository
620                    .get_by_name(NetworkType::Stellar, &relayer.network)
621                    .await
622                    .ok()
623                    .flatten()
624                    .ok_or_else(|| {
625                        TransactionError::NetworkConfiguration(format!(
626                            "Network {} not found",
627                            relayer.network
628                        ))
629                    })?;
630
631                let network = StellarNetwork::try_from(network_repo)
632                    .map_err(|e| TransactionError::NetworkConfiguration(e.to_string()))?;
633
634                let stellar_provider =
635                    get_network_provider(&network, relayer.custom_rpc_urls.clone())
636                        .map_err(|e| TransactionError::NetworkConfiguration(e.to_string()))?;
637
638                // Create DEX service for swap operations and validations
639                let horizon_url = network.horizon_url.clone().unwrap_or_else(|| {
640                    if network.is_testnet() {
641                        STELLAR_HORIZON_TESTNET_URL.to_string()
642                    } else {
643                        STELLAR_HORIZON_MAINNET_URL.to_string()
644                    }
645                });
646                let provider_arc = Arc::new(stellar_provider.clone());
647                // Clone Arc for DEX service (cheap - just increments reference count)
648                let signer_arc = signer_service.clone();
649
650                // Get strategies from relayer policy (default to OrderBook if none specified)
651                let strategies = relayer
652                    .policies
653                    .get_stellar_policy()
654                    .get_swap_config()
655                    .and_then(|config| {
656                        if config.strategies.is_empty() {
657                            None
658                        } else {
659                            Some(config.strategies.clone())
660                        }
661                    })
662                    .unwrap_or_else(|| vec![StellarSwapStrategy::OrderBook]);
663
664                // Create DEX services for each configured strategy
665                let mut dex_services: Vec<DexServiceWrapper<_, _>> = Vec::new();
666                for strategy in &strategies {
667                    match strategy {
668                        StellarSwapStrategy::OrderBook => {
669                            let order_book_service = Arc::new(
670                                OrderBookService::new(
671                                    horizon_url.clone(),
672                                    provider_arc.clone(),
673                                    signer_arc.clone(),
674                                )
675                                .map_err(|e| {
676                                    TransactionError::NetworkConfiguration(format!(
677                                        "Failed to create OrderBook DEX service: {e}"
678                                    ))
679                                })?,
680                            );
681                            dex_services.push(DexServiceWrapper::OrderBook(order_book_service));
682                        }
683                        StellarSwapStrategy::Soroswap => {
684                            let is_testnet = network.is_testnet();
685                            let network_label = if is_testnet { "TESTNET" } else { "MAINNET" };
686
687                            let router_address =
688                                crate::config::ServerConfig::resolve_stellar_soroswap_router_address(is_testnet)
689                                    .ok_or_else(|| {
690                                        eyre::eyre!(
691                                            "Soroswap router address not configured. Set STELLAR_{network_label}_SOROSWAP_ROUTER_ADDRESS env var."
692                                        )
693                                    })?;
694                            let factory_address =
695                                crate::config::ServerConfig::resolve_stellar_soroswap_factory_address(is_testnet)
696                                    .ok_or_else(|| {
697                                        eyre::eyre!(
698                                            "Soroswap factory address not configured. Set STELLAR_{network_label}_SOROSWAP_FACTORY_ADDRESS env var."
699                                        )
700                                    })?;
701                            let native_wrapper_address =
702                                crate::config::ServerConfig::resolve_stellar_soroswap_native_wrapper_address(is_testnet)
703                                    .ok_or_else(|| {
704                                        eyre::eyre!(
705                                            "Soroswap native wrapper address not configured. Set STELLAR_{network_label}_SOROSWAP_NATIVE_WRAPPER_ADDRESS env var."
706                                        )
707                                    })?;
708
709                            let soroswap_service = Arc::new(SoroswapService::new(
710                                router_address,
711                                factory_address,
712                                native_wrapper_address,
713                                provider_arc.clone(),
714                                network.passphrase.clone(),
715                            ));
716                            dex_services.push(DexServiceWrapper::Soroswap(soroswap_service));
717                        }
718                    }
719                }
720
721                // Create multi-strategy DEX service
722                let dex_service = Arc::new(StellarDexService::new(dex_services));
723
724                Ok(NetworkTransaction::Stellar(DefaultStellarTransaction::new(
725                    relayer,
726                    relayer_repository,
727                    transaction_repository,
728                    job_producer,
729                    signer_service,
730                    stellar_provider,
731                    transaction_counter_store,
732                    dex_service,
733                )?))
734            }
735        }
736    }
737}