openzeppelin_relayer/services/stellar_dex/
stellar_dex_service.rs1use 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#[derive(Clone)]
22pub enum DexServiceWrapper<P, S>
23where
24 P: StellarProviderTrait + Send + Sync + 'static,
25 S: StellarSignTrait + Signer + Send + Sync + 'static,
26{
27 OrderBook(Arc<OrderBookService<P, S>>),
29 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
53pub struct StellarDexService<P, S>
61where
62 P: StellarProviderTrait + Send + Sync + 'static,
63 S: StellarSignTrait + Signer + Send + Sync + 'static,
64{
65 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 pub fn new(strategies: Vec<DexServiceWrapper<P, S>>) -> Self {
79 Self { strategies }
80 }
81
82 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 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 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(¶ms.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(¶ms.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 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 const CLASSIC_ASSET: &str = "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
276 const CONTRACT_ASSET: &str = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
277
278 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 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 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 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 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 fn create_empty_service() -> StellarDexService<MockStellarProviderTrait, MockCombinedSigner> {
329 StellarDexService::new(vec![])
330 }
331
332 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 #[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 #[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 #[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 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 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 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 let service = create_multi_strategy_service();
476 let strategy = service.find_strategy_for_asset("native");
477 assert!(strategy.is_some());
478 assert!(matches!(strategy.unwrap(), DexServiceWrapper::OrderBook(_)));
480 }
481
482 #[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 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 #[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 #[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 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 #[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 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 #[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 #[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}