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, 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
28    OrderBook(Arc<OrderBookService<P, S>>),
29    // TODO: Add Soroswap variant when implemented
30    // Soroswap(Arc<SoroswapService<P, S>>),
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            } // DexServiceWrapper::Soroswap(svc) => {
141              //     svc.get_token_to_xlm_quote(asset_id, amount, slippage, asset_decimals)
142              //         .await
143              // }
144        }
145    }
146
147    async fn get_xlm_to_token_quote(
148        &self,
149        asset_id: &str,
150        amount: u64,
151        slippage: f32,
152        asset_decimals: Option<u8>,
153    ) -> Result<StellarQuoteResponse, StellarDexServiceError> {
154        let strategy = self.find_strategy_for_asset(asset_id).ok_or_else(|| {
155            StellarDexServiceError::InvalidAssetIdentifier(format!(
156                "No configured strategy can handle asset: {asset_id}"
157            ))
158        })?;
159
160        match strategy {
161            DexServiceWrapper::OrderBook(svc) => {
162                svc.get_xlm_to_token_quote(asset_id, amount, slippage, asset_decimals)
163                    .await
164            } // DexServiceWrapper::Soroswap(svc) => {
165              //     svc.get_xlm_to_token_quote(asset_id, amount, slippage, asset_decimals)
166              //         .await
167              // }
168        }
169    }
170
171    async fn prepare_swap_transaction(
172        &self,
173        params: SwapTransactionParams,
174    ) -> Result<(String, StellarQuoteResponse), StellarDexServiceError> {
175        let strategy = self
176            .find_strategy_for_asset(&params.source_asset)
177            .ok_or_else(|| {
178                StellarDexServiceError::InvalidAssetIdentifier(format!(
179                    "No configured strategy can handle asset: {}",
180                    params.source_asset
181                ))
182            })?;
183
184        match strategy {
185            DexServiceWrapper::OrderBook(svc) => svc.prepare_swap_transaction(params).await,
186            // DexServiceWrapper::Soroswap(svc) => svc.prepare_swap_transaction(params).await,
187        }
188    }
189
190    async fn execute_swap(
191        &self,
192        params: SwapTransactionParams,
193    ) -> Result<SwapExecutionResult, StellarDexServiceError> {
194        let strategy = self
195            .find_strategy_for_asset(&params.source_asset)
196            .ok_or_else(|| {
197                StellarDexServiceError::InvalidAssetIdentifier(format!(
198                    "No configured strategy can handle asset: {}",
199                    params.source_asset
200                ))
201            })?;
202
203        match strategy {
204            DexServiceWrapper::OrderBook(svc) => svc.execute_swap(params).await,
205            // DexServiceWrapper::Soroswap(svc) => svc.execute_swap(params).await,
206        }
207    }
208}