openzeppelin_relayer/services/stellar_dex/
stellar_dex_service.rs

1//! Multi-strategy Stellar DEX service implementation
2//!
3//! This module provides a DEX service that automatically selects the appropriate strategy
4//! based on asset type and configured strategies. It implements `StellarDexServiceTrait`
5//! and internally routes calls to the first strategy that can handle the requested asset.
6
7use super::{
8    AssetType, OrderBookService, SoroswapService, StellarDexServiceError, StellarDexServiceTrait,
9    StellarQuoteResponse, SwapExecutionResult, SwapTransactionParams,
10};
11use crate::services::{provider::StellarProviderTrait, signer::Signer, signer::StellarSignTrait};
12use async_trait::async_trait;
13use std::collections::HashSet;
14use std::sync::Arc;
15use tracing::debug;
16
17/// Enum wrapper for different DEX service implementations
18///
19/// This enum allows storing different concrete DEX service types in a collection
20/// without using dynamic dispatch (`dyn`).
21#[derive(Clone)]
22pub enum DexServiceWrapper<P, S>
23where
24    P: StellarProviderTrait + Send + Sync + 'static,
25    S: StellarSignTrait + Signer + Send + Sync + 'static,
26{
27    /// Order Book DEX service (for classic Stellar assets)
28    OrderBook(Arc<OrderBookService<P, S>>),
29    /// Soroswap DEX service (for Soroban contract tokens)
30    Soroswap(Arc<SoroswapService<P>>),
31}
32
33impl<P, S> DexServiceWrapper<P, S>
34where
35    P: StellarProviderTrait + Send + Sync + 'static,
36    S: StellarSignTrait + Signer + Send + Sync + 'static,
37{
38    fn can_handle_asset(&self, asset_id: &str) -> bool {
39        match self {
40            DexServiceWrapper::OrderBook(service) => service.can_handle_asset(asset_id),
41            DexServiceWrapper::Soroswap(service) => service.can_handle_asset(asset_id),
42        }
43    }
44
45    fn supported_asset_types(&self) -> HashSet<AssetType> {
46        match self {
47            DexServiceWrapper::OrderBook(service) => service.supported_asset_types(),
48            DexServiceWrapper::Soroswap(service) => service.supported_asset_types(),
49        }
50    }
51}
52
53/// Multi-strategy Stellar DEX service
54///
55/// This service maintains a list of DEX strategy implementations (one per configured strategy)
56/// and automatically selects the first one that can handle a given asset when methods are called.
57/// The routing logic is integrated directly into the service implementation.
58///
59/// Uses static dispatch via generics and an enum wrapper instead of dynamic dispatch.
60pub struct StellarDexService<P, S>
61where
62    P: StellarProviderTrait + Send + Sync + 'static,
63    S: StellarSignTrait + Signer + Send + Sync + 'static,
64{
65    /// List of DEX strategy implementations in priority order (matching the configured strategies)
66    strategies: Vec<DexServiceWrapper<P, S>>,
67}
68
69impl<P, S> StellarDexService<P, S>
70where
71    P: StellarProviderTrait + Send + Sync + 'static,
72    S: StellarSignTrait + Signer + Send + Sync + 'static,
73{
74    /// Create a new multi-strategy DEX service with the given strategy implementations
75    ///
76    /// # Arguments
77    /// * `strategies` - Vector of DEX strategy implementations in priority order
78    pub fn new(strategies: Vec<DexServiceWrapper<P, S>>) -> Self {
79        Self { strategies }
80    }
81
82    /// Find the first strategy that can handle the given asset
83    ///
84    /// # Arguments
85    /// * `asset_id` - Asset identifier to check
86    ///
87    /// # Returns
88    /// `Some(strategy)` if a strategy can handle the asset, `None` otherwise
89    fn find_strategy_for_asset(&self, asset_id: &str) -> Option<&DexServiceWrapper<P, S>> {
90        for strategy in &self.strategies {
91            if strategy.can_handle_asset(asset_id) {
92                debug!(
93                    asset_id = %asset_id,
94                    "Selected DEX strategy that can handle asset"
95                );
96                return Some(strategy);
97            }
98        }
99        None
100    }
101}
102
103#[async_trait]
104impl<P, S> StellarDexServiceTrait for StellarDexService<P, S>
105where
106    P: StellarProviderTrait + Send + Sync + 'static,
107    S: StellarSignTrait + Signer + Send + Sync + 'static,
108{
109    fn supported_asset_types(&self) -> HashSet<AssetType> {
110        // Return the union of all supported asset types from all strategies
111        let mut types = HashSet::new();
112        for strategy in &self.strategies {
113            types.extend(strategy.supported_asset_types());
114        }
115        types
116    }
117
118    fn can_handle_asset(&self, asset_id: &str) -> bool {
119        // Check if any strategy can handle this asset
120        self.find_strategy_for_asset(asset_id).is_some()
121    }
122
123    async fn get_token_to_xlm_quote(
124        &self,
125        asset_id: &str,
126        amount: u64,
127        slippage: f32,
128        asset_decimals: Option<u8>,
129    ) -> Result<StellarQuoteResponse, StellarDexServiceError> {
130        let strategy = self.find_strategy_for_asset(asset_id).ok_or_else(|| {
131            StellarDexServiceError::InvalidAssetIdentifier(format!(
132                "No configured strategy can handle asset: {asset_id}"
133            ))
134        })?;
135
136        match strategy {
137            DexServiceWrapper::OrderBook(svc) => {
138                svc.get_token_to_xlm_quote(asset_id, amount, slippage, asset_decimals)
139                    .await
140            }
141            DexServiceWrapper::Soroswap(svc) => {
142                svc.get_token_to_xlm_quote(asset_id, amount, slippage, asset_decimals)
143                    .await
144            }
145        }
146    }
147
148    async fn get_xlm_to_token_quote(
149        &self,
150        asset_id: &str,
151        amount: u64,
152        slippage: f32,
153        asset_decimals: Option<u8>,
154    ) -> Result<StellarQuoteResponse, StellarDexServiceError> {
155        let strategy = self.find_strategy_for_asset(asset_id).ok_or_else(|| {
156            StellarDexServiceError::InvalidAssetIdentifier(format!(
157                "No configured strategy can handle asset: {asset_id}"
158            ))
159        })?;
160
161        match strategy {
162            DexServiceWrapper::OrderBook(svc) => {
163                svc.get_xlm_to_token_quote(asset_id, amount, slippage, asset_decimals)
164                    .await
165            }
166            DexServiceWrapper::Soroswap(svc) => {
167                svc.get_xlm_to_token_quote(asset_id, amount, slippage, asset_decimals)
168                    .await
169            }
170        }
171    }
172
173    async fn prepare_swap_transaction(
174        &self,
175        params: SwapTransactionParams,
176    ) -> Result<(String, StellarQuoteResponse), StellarDexServiceError> {
177        let strategy = self
178            .find_strategy_for_asset(&params.source_asset)
179            .ok_or_else(|| {
180                StellarDexServiceError::InvalidAssetIdentifier(format!(
181                    "No configured strategy can handle asset: {}",
182                    params.source_asset
183                ))
184            })?;
185
186        match strategy {
187            DexServiceWrapper::OrderBook(svc) => svc.prepare_swap_transaction(params).await,
188            DexServiceWrapper::Soroswap(svc) => svc.prepare_swap_transaction(params).await,
189        }
190    }
191
192    async fn execute_swap(
193        &self,
194        params: SwapTransactionParams,
195    ) -> Result<SwapExecutionResult, StellarDexServiceError> {
196        let strategy = self
197            .find_strategy_for_asset(&params.source_asset)
198            .ok_or_else(|| {
199                StellarDexServiceError::InvalidAssetIdentifier(format!(
200                    "No configured strategy can handle asset: {}",
201                    params.source_asset
202                ))
203            })?;
204
205        match strategy {
206            DexServiceWrapper::OrderBook(svc) => svc.execute_swap(params).await,
207            DexServiceWrapper::Soroswap(svc) => svc.execute_swap(params).await,
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::models::SignerError;
216    use crate::services::provider::MockStellarProviderTrait;
217    use crate::services::signer::{MockStellarSignTrait, Signer};
218    use async_trait::async_trait;
219
220    // ==================== Mock Setup ====================
221
222    /// Combined mock that implements both StellarSignTrait and Signer
223    struct MockCombinedSigner {
224        stellar_mock: MockStellarSignTrait,
225    }
226
227    impl MockCombinedSigner {
228        fn new() -> Self {
229            Self {
230                stellar_mock: MockStellarSignTrait::new(),
231            }
232        }
233    }
234
235    #[async_trait]
236    impl StellarSignTrait for MockCombinedSigner {
237        async fn sign_xdr_transaction(
238            &self,
239            unsigned_xdr: &str,
240            network_passphrase: &str,
241        ) -> Result<crate::domain::relayer::SignXdrTransactionResponseStellar, SignerError>
242        {
243            self.stellar_mock
244                .sign_xdr_transaction(unsigned_xdr, network_passphrase)
245                .await
246        }
247    }
248
249    #[async_trait]
250    impl Signer for MockCombinedSigner {
251        async fn address(&self) -> Result<crate::models::Address, SignerError> {
252            Ok(crate::models::Address::Stellar(
253                "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
254            ))
255        }
256
257        async fn sign_transaction(
258            &self,
259            _transaction: crate::models::NetworkTransactionData,
260        ) -> Result<crate::domain::SignTransactionResponse, SignerError> {
261            Ok(crate::domain::SignTransactionResponse::Stellar(
262                crate::domain::SignTransactionResponseStellar {
263                    signature: crate::models::DecoratedSignature {
264                        hint: soroban_rs::xdr::SignatureHint([0; 4]),
265                        signature: soroban_rs::xdr::Signature(
266                            soroban_rs::xdr::BytesM::try_from(vec![0u8; 64]).unwrap(),
267                        ),
268                    },
269                },
270            ))
271        }
272    }
273
274    // Test constants
275    const CLASSIC_ASSET: &str = "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
276    const CONTRACT_ASSET: &str = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
277
278    /// Create a test OrderBookService wrapped in Arc
279    fn create_order_book_service(
280    ) -> Arc<OrderBookService<MockStellarProviderTrait, MockCombinedSigner>> {
281        let provider = Arc::new(MockStellarProviderTrait::new());
282        let signer = Arc::new(MockCombinedSigner::new());
283        Arc::new(
284            OrderBookService::new(
285                "https://horizon-testnet.stellar.org".to_string(),
286                provider,
287                signer,
288            )
289            .expect("Failed to create OrderBookService"),
290        )
291    }
292
293    /// Create a test SoroswapService wrapped in Arc
294    fn create_soroswap_service() -> Arc<SoroswapService<MockStellarProviderTrait>> {
295        let provider = Arc::new(MockStellarProviderTrait::new());
296        Arc::new(SoroswapService::new(
297            "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
298            "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA".to_string(),
299            "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
300            provider,
301            "Test SDF Network ; September 2015".to_string(),
302        ))
303    }
304
305    /// Create a StellarDexService with both OrderBook and Soroswap strategies
306    fn create_multi_strategy_service(
307    ) -> StellarDexService<MockStellarProviderTrait, MockCombinedSigner> {
308        let order_book = DexServiceWrapper::OrderBook(create_order_book_service());
309        let soroswap = DexServiceWrapper::Soroswap(create_soroswap_service());
310        StellarDexService::new(vec![order_book, soroswap])
311    }
312
313    /// Create a StellarDexService with only OrderBook strategy
314    fn create_order_book_only_service(
315    ) -> StellarDexService<MockStellarProviderTrait, MockCombinedSigner> {
316        let order_book = DexServiceWrapper::OrderBook(create_order_book_service());
317        StellarDexService::new(vec![order_book])
318    }
319
320    /// Create a StellarDexService with only Soroswap strategy
321    fn create_soroswap_only_service(
322    ) -> StellarDexService<MockStellarProviderTrait, MockCombinedSigner> {
323        let soroswap = DexServiceWrapper::Soroswap(create_soroswap_service());
324        StellarDexService::new(vec![soroswap])
325    }
326
327    /// Create a StellarDexService with no strategies
328    fn create_empty_service() -> StellarDexService<MockStellarProviderTrait, MockCombinedSigner> {
329        StellarDexService::new(vec![])
330    }
331
332    /// Create SwapTransactionParams for testing
333    fn create_swap_params(source_asset: &str) -> SwapTransactionParams {
334        SwapTransactionParams {
335            source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
336            source_asset: source_asset.to_string(),
337            destination_asset: "native".to_string(),
338            amount: 1000000,
339            slippage_percent: 1.0,
340            network_passphrase: "Test SDF Network ; September 2015".to_string(),
341            source_asset_decimals: Some(7),
342            destination_asset_decimals: Some(7),
343        }
344    }
345
346    // ==================== DexServiceWrapper Tests ====================
347
348    #[test]
349    fn test_wrapper_order_book_can_handle_native() {
350        let wrapper: DexServiceWrapper<MockStellarProviderTrait, MockCombinedSigner> =
351            DexServiceWrapper::OrderBook(create_order_book_service());
352        assert!(wrapper.can_handle_asset("native"));
353    }
354
355    #[test]
356    fn test_wrapper_order_book_can_handle_classic_asset() {
357        let wrapper: DexServiceWrapper<MockStellarProviderTrait, MockCombinedSigner> =
358            DexServiceWrapper::OrderBook(create_order_book_service());
359        assert!(wrapper.can_handle_asset(CLASSIC_ASSET));
360    }
361
362    #[test]
363    fn test_wrapper_order_book_cannot_handle_contract() {
364        let wrapper: DexServiceWrapper<MockStellarProviderTrait, MockCombinedSigner> =
365            DexServiceWrapper::OrderBook(create_order_book_service());
366        assert!(!wrapper.can_handle_asset(CONTRACT_ASSET));
367    }
368
369    #[test]
370    fn test_wrapper_soroswap_can_handle_native() {
371        let wrapper: DexServiceWrapper<MockStellarProviderTrait, MockCombinedSigner> =
372            DexServiceWrapper::Soroswap(create_soroswap_service());
373        assert!(wrapper.can_handle_asset("native"));
374    }
375
376    #[test]
377    fn test_wrapper_soroswap_can_handle_contract() {
378        let wrapper: DexServiceWrapper<MockStellarProviderTrait, MockCombinedSigner> =
379            DexServiceWrapper::Soroswap(create_soroswap_service());
380        assert!(wrapper.can_handle_asset(CONTRACT_ASSET));
381    }
382
383    #[test]
384    fn test_wrapper_soroswap_cannot_handle_classic_asset() {
385        let wrapper: DexServiceWrapper<MockStellarProviderTrait, MockCombinedSigner> =
386            DexServiceWrapper::Soroswap(create_soroswap_service());
387        assert!(!wrapper.can_handle_asset(CLASSIC_ASSET));
388    }
389
390    #[test]
391    fn test_wrapper_order_book_supported_asset_types() {
392        let wrapper: DexServiceWrapper<MockStellarProviderTrait, MockCombinedSigner> =
393            DexServiceWrapper::OrderBook(create_order_book_service());
394        let types = wrapper.supported_asset_types();
395        assert!(types.contains(&AssetType::Native));
396        assert!(types.contains(&AssetType::Classic));
397        assert!(!types.contains(&AssetType::Contract));
398    }
399
400    #[test]
401    fn test_wrapper_soroswap_supported_asset_types() {
402        let wrapper: DexServiceWrapper<MockStellarProviderTrait, MockCombinedSigner> =
403            DexServiceWrapper::Soroswap(create_soroswap_service());
404        let types = wrapper.supported_asset_types();
405        assert!(types.contains(&AssetType::Native));
406        assert!(types.contains(&AssetType::Contract));
407        assert!(!types.contains(&AssetType::Classic));
408    }
409
410    // ==================== StellarDexService Constructor Tests ====================
411
412    #[test]
413    fn test_new_with_multiple_strategies() {
414        let service = create_multi_strategy_service();
415        assert_eq!(service.strategies.len(), 2);
416    }
417
418    #[test]
419    fn test_new_with_single_strategy() {
420        let service = create_order_book_only_service();
421        assert_eq!(service.strategies.len(), 1);
422    }
423
424    #[test]
425    fn test_new_with_empty_strategies() {
426        let service = create_empty_service();
427        assert_eq!(service.strategies.len(), 0);
428    }
429
430    // ==================== find_strategy_for_asset Tests ====================
431
432    #[test]
433    fn test_find_strategy_for_native_asset() {
434        let service = create_multi_strategy_service();
435        let strategy = service.find_strategy_for_asset("native");
436        assert!(strategy.is_some());
437    }
438
439    #[test]
440    fn test_find_strategy_for_classic_asset() {
441        let service = create_multi_strategy_service();
442        let strategy = service.find_strategy_for_asset(CLASSIC_ASSET);
443        assert!(strategy.is_some());
444        // Verify it's the OrderBook strategy
445        assert!(matches!(strategy.unwrap(), DexServiceWrapper::OrderBook(_)));
446    }
447
448    #[test]
449    fn test_find_strategy_for_contract_asset() {
450        let service = create_multi_strategy_service();
451        let strategy = service.find_strategy_for_asset(CONTRACT_ASSET);
452        assert!(strategy.is_some());
453        // Verify it's the Soroswap strategy
454        assert!(matches!(strategy.unwrap(), DexServiceWrapper::Soroswap(_)));
455    }
456
457    #[test]
458    fn test_find_strategy_returns_none_for_unhandled_asset() {
459        let service = create_order_book_only_service();
460        // Contract assets are not handled by OrderBook
461        let strategy = service.find_strategy_for_asset(CONTRACT_ASSET);
462        assert!(strategy.is_none());
463    }
464
465    #[test]
466    fn test_find_strategy_returns_none_for_empty_service() {
467        let service = create_empty_service();
468        let strategy = service.find_strategy_for_asset("native");
469        assert!(strategy.is_none());
470    }
471
472    #[test]
473    fn test_find_strategy_priority_order() {
474        // OrderBook comes first, so it should handle native assets
475        let service = create_multi_strategy_service();
476        let strategy = service.find_strategy_for_asset("native");
477        assert!(strategy.is_some());
478        // First strategy (OrderBook) should be selected for native
479        assert!(matches!(strategy.unwrap(), DexServiceWrapper::OrderBook(_)));
480    }
481
482    // ==================== StellarDexServiceTrait::supported_asset_types Tests ====================
483
484    #[test]
485    fn test_supported_asset_types_union_of_all_strategies() {
486        let service = create_multi_strategy_service();
487        let types = service.supported_asset_types();
488        // Should include Native, Classic (from OrderBook), and Contract (from Soroswap)
489        assert!(types.contains(&AssetType::Native));
490        assert!(types.contains(&AssetType::Classic));
491        assert!(types.contains(&AssetType::Contract));
492        assert_eq!(types.len(), 3);
493    }
494
495    #[test]
496    fn test_supported_asset_types_order_book_only() {
497        let service = create_order_book_only_service();
498        let types = service.supported_asset_types();
499        assert!(types.contains(&AssetType::Native));
500        assert!(types.contains(&AssetType::Classic));
501        assert!(!types.contains(&AssetType::Contract));
502    }
503
504    #[test]
505    fn test_supported_asset_types_soroswap_only() {
506        let service = create_soroswap_only_service();
507        let types = service.supported_asset_types();
508        assert!(types.contains(&AssetType::Native));
509        assert!(types.contains(&AssetType::Contract));
510        assert!(!types.contains(&AssetType::Classic));
511    }
512
513    #[test]
514    fn test_supported_asset_types_empty_service() {
515        let service = create_empty_service();
516        let types = service.supported_asset_types();
517        assert!(types.is_empty());
518    }
519
520    // ==================== StellarDexServiceTrait::can_handle_asset Tests ====================
521
522    #[test]
523    fn test_can_handle_asset_native() {
524        let service = create_multi_strategy_service();
525        assert!(service.can_handle_asset("native"));
526    }
527
528    #[test]
529    fn test_can_handle_asset_empty_string() {
530        let service = create_multi_strategy_service();
531        assert!(service.can_handle_asset(""));
532    }
533
534    #[test]
535    fn test_can_handle_asset_classic() {
536        let service = create_multi_strategy_service();
537        assert!(service.can_handle_asset(CLASSIC_ASSET));
538    }
539
540    #[test]
541    fn test_can_handle_asset_contract() {
542        let service = create_multi_strategy_service();
543        assert!(service.can_handle_asset(CONTRACT_ASSET));
544    }
545
546    #[test]
547    fn test_cannot_handle_contract_with_order_book_only() {
548        let service = create_order_book_only_service();
549        assert!(!service.can_handle_asset(CONTRACT_ASSET));
550    }
551
552    #[test]
553    fn test_cannot_handle_classic_with_soroswap_only() {
554        let service = create_soroswap_only_service();
555        assert!(!service.can_handle_asset(CLASSIC_ASSET));
556    }
557
558    #[test]
559    fn test_cannot_handle_any_asset_with_empty_service() {
560        let service = create_empty_service();
561        assert!(!service.can_handle_asset("native"));
562        assert!(!service.can_handle_asset(CLASSIC_ASSET));
563        assert!(!service.can_handle_asset(CONTRACT_ASSET));
564    }
565
566    #[test]
567    fn test_cannot_handle_invalid_asset() {
568        let service = create_multi_strategy_service();
569        assert!(!service.can_handle_asset("INVALID"));
570        assert!(!service.can_handle_asset("random_string"));
571    }
572
573    // ==================== get_token_to_xlm_quote Error Tests ====================
574
575    #[tokio::test]
576    async fn test_get_token_to_xlm_quote_no_strategy_error() {
577        let service = create_empty_service();
578        let result = service
579            .get_token_to_xlm_quote("native", 1000000, 1.0, Some(7))
580            .await;
581        assert!(result.is_err());
582        match result.unwrap_err() {
583            StellarDexServiceError::InvalidAssetIdentifier(msg) => {
584                assert!(msg.contains("No configured strategy can handle asset"));
585            }
586            _ => panic!("Expected InvalidAssetIdentifier error"),
587        }
588    }
589
590    #[tokio::test]
591    async fn test_get_token_to_xlm_quote_unhandled_asset_error() {
592        let service = create_order_book_only_service();
593        // Contract assets are not handled by OrderBook
594        let result = service
595            .get_token_to_xlm_quote(CONTRACT_ASSET, 1000000, 1.0, Some(7))
596            .await;
597        assert!(result.is_err());
598        match result.unwrap_err() {
599            StellarDexServiceError::InvalidAssetIdentifier(msg) => {
600                assert!(msg.contains("No configured strategy can handle asset"));
601                assert!(msg.contains(CONTRACT_ASSET));
602            }
603            _ => panic!("Expected InvalidAssetIdentifier error"),
604        }
605    }
606
607    // ==================== get_xlm_to_token_quote Error Tests ====================
608
609    #[tokio::test]
610    async fn test_get_xlm_to_token_quote_no_strategy_error() {
611        let service = create_empty_service();
612        let result = service
613            .get_xlm_to_token_quote("native", 1000000, 1.0, Some(7))
614            .await;
615        assert!(result.is_err());
616        match result.unwrap_err() {
617            StellarDexServiceError::InvalidAssetIdentifier(msg) => {
618                assert!(msg.contains("No configured strategy can handle asset"));
619            }
620            _ => panic!("Expected InvalidAssetIdentifier error"),
621        }
622    }
623
624    #[tokio::test]
625    async fn test_get_xlm_to_token_quote_unhandled_asset_error() {
626        let service = create_soroswap_only_service();
627        // Classic assets are not handled by Soroswap
628        let result = service
629            .get_xlm_to_token_quote(CLASSIC_ASSET, 1000000, 1.0, Some(7))
630            .await;
631        assert!(result.is_err());
632        match result.unwrap_err() {
633            StellarDexServiceError::InvalidAssetIdentifier(msg) => {
634                assert!(msg.contains("No configured strategy can handle asset"));
635                assert!(msg.contains(CLASSIC_ASSET));
636            }
637            _ => panic!("Expected InvalidAssetIdentifier error"),
638        }
639    }
640
641    // ==================== prepare_swap_transaction Error Tests ====================
642
643    #[tokio::test]
644    async fn test_prepare_swap_transaction_no_strategy_error() {
645        let service = create_empty_service();
646        let params = create_swap_params("native");
647        let result = service.prepare_swap_transaction(params).await;
648        assert!(result.is_err());
649        match result.unwrap_err() {
650            StellarDexServiceError::InvalidAssetIdentifier(msg) => {
651                assert!(msg.contains("No configured strategy can handle asset"));
652            }
653            _ => panic!("Expected InvalidAssetIdentifier error"),
654        }
655    }
656
657    #[tokio::test]
658    async fn test_prepare_swap_transaction_unhandled_asset_error() {
659        let service = create_order_book_only_service();
660        let params = create_swap_params(CONTRACT_ASSET);
661        let result = service.prepare_swap_transaction(params).await;
662        assert!(result.is_err());
663        match result.unwrap_err() {
664            StellarDexServiceError::InvalidAssetIdentifier(msg) => {
665                assert!(msg.contains("No configured strategy can handle asset"));
666                assert!(msg.contains(CONTRACT_ASSET));
667            }
668            _ => panic!("Expected InvalidAssetIdentifier error"),
669        }
670    }
671
672    // ==================== execute_swap Error Tests ====================
673
674    #[tokio::test]
675    async fn test_execute_swap_no_strategy_error() {
676        let service = create_empty_service();
677        let params = create_swap_params("native");
678        let result = service.execute_swap(params).await;
679        assert!(result.is_err());
680        match result.unwrap_err() {
681            StellarDexServiceError::InvalidAssetIdentifier(msg) => {
682                assert!(msg.contains("No configured strategy can handle asset"));
683            }
684            _ => panic!("Expected InvalidAssetIdentifier error"),
685        }
686    }
687
688    #[tokio::test]
689    async fn test_execute_swap_unhandled_asset_error() {
690        let service = create_soroswap_only_service();
691        let params = create_swap_params(CLASSIC_ASSET);
692        let result = service.execute_swap(params).await;
693        assert!(result.is_err());
694        match result.unwrap_err() {
695            StellarDexServiceError::InvalidAssetIdentifier(msg) => {
696                assert!(msg.contains("No configured strategy can handle asset"));
697                assert!(msg.contains(CLASSIC_ASSET));
698            }
699            _ => panic!("Expected InvalidAssetIdentifier error"),
700        }
701    }
702}