1use async_trait::async_trait;
7use futures::future::join_all;
8use tracing::{debug, error, info};
9
10use crate::constants::DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE;
11use crate::domain::relayer::{
12 Relayer, RelayerError, StellarRelayer, StellarRelayerDexTrait, SwapResult,
13};
14use crate::domain::transaction::stellar::token::get_token_balance;
15use crate::jobs::JobProducerTrait;
16use crate::models::transaction::request::StellarTransactionRequest;
17use crate::models::{
18 produce_stellar_dex_webhook_payload, NetworkTransactionRequest, RelayerRepoModel,
19 StellarDexPayload, StellarFeePaymentStrategy,
20};
21use crate::models::{NetworkRepoModel, TransactionRepoModel};
22use crate::repositories::{
23 NetworkRepository, RelayerRepository, Repository, TransactionRepository,
24};
25use crate::services::provider::StellarProviderTrait;
26use crate::services::signer::StellarSignTrait;
27use crate::services::stellar_dex::{StellarDexServiceTrait, SwapTransactionParams};
28use crate::services::TransactionCounterServiceTrait;
29
30#[async_trait]
31impl<P, RR, NR, TR, J, TCS, S, D> StellarRelayerDexTrait
32 for StellarRelayer<P, RR, NR, TR, J, TCS, S, D>
33where
34 P: StellarProviderTrait + Send + Sync,
35 D: StellarDexServiceTrait + Send + Sync + 'static,
36 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
37 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
38 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
39 J: JobProducerTrait + Send + Sync + 'static,
40 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
41 S: StellarSignTrait + Send + Sync + 'static,
42{
43 async fn handle_token_swap_request(
52 &self,
53 relayer_id: String,
54 ) -> Result<Vec<SwapResult>, RelayerError> {
55 debug!("handling token swap request for relayer {}", relayer_id);
56 let relayer = self
57 .relayer_repository
58 .get_by_id(relayer_id.clone())
59 .await?;
60
61 let policy = relayer.policies.get_stellar_policy();
62
63 if !matches!(
66 policy.fee_payment_strategy,
67 Some(StellarFeePaymentStrategy::User)
68 ) {
69 debug!(
70 %relayer_id,
71 "Token swap is only supported for user fee payment strategy; Exiting."
72 );
73 return Ok(vec![]);
74 }
75
76 let swap_config = match policy.get_swap_config() {
77 Some(config) => config,
78 None => {
79 debug!(%relayer_id, "No swap configuration specified for relayer; Exiting.");
80 return Ok(vec![]);
81 }
82 };
83
84 let strategies = &swap_config.strategies;
85 if strategies.is_empty() {
86 debug!(%relayer_id, "No swap strategies specified for relayer; Exiting.");
87 return Ok(vec![]);
88 }
89
90 let tokens_to_swap = {
92 let mut eligible_tokens = Vec::new();
93
94 let allowed_tokens = policy.get_allowed_tokens();
95 if allowed_tokens.is_empty() {
96 debug!(%relayer_id, "No allowed tokens configured for swap");
97 return Ok(vec![]);
98 }
99
100 for token in &allowed_tokens {
101 let token_balance =
103 match get_token_balance(&self.provider, &relayer.address, &token.asset).await {
104 Ok(balance) => balance,
105 Err(e) => {
106 error!(
107 %relayer_id,
108 token = %token.asset,
109 error = %e,
110 "Failed to get token balance, skipping this token"
111 );
112 continue;
113 }
114 };
115
116 let swap_amount = calculate_swap_amount(
118 token_balance,
119 token
120 .swap_config
121 .as_ref()
122 .and_then(|config| config.min_amount),
123 token
124 .swap_config
125 .as_ref()
126 .and_then(|config| config.max_amount),
127 token
128 .swap_config
129 .as_ref()
130 .and_then(|config| config.retain_min_amount),
131 )
132 .unwrap_or(0);
133
134 if swap_amount > 0 {
135 debug!(%relayer_id, token = ?token.asset, "token swap eligible for token");
136
137 eligible_tokens.push((
139 token.asset.clone(),
140 swap_amount,
141 token
142 .swap_config
143 .as_ref()
144 .and_then(|config| config.slippage_percentage)
145 .unwrap_or(DEFAULT_CONVERSION_SLIPPAGE_PERCENTAGE),
146 ));
147 }
148 }
149
150 eligible_tokens
151 };
152 let network_passphrase = self.network.passphrase.clone();
153 let relayer_network = relayer.network.clone();
154
155 let swap_prep_futures: Vec<_> = tokens_to_swap
162 .iter()
163 .filter_map(|(token_asset, swap_amount, slippage_percent)| {
164 if !self.dex_service.can_handle_asset(token_asset) {
166 debug!(
167 %relayer_id,
168 token = ?token_asset,
169 "Skipping token swap - no configured strategy can handle this asset type"
170 );
171 return None;
172 }
173
174 let token_asset = token_asset.clone();
175 let dex_service = self.dex_service.clone();
176 let relayer_address = relayer.address.clone();
177 let relayer_id_clone = relayer_id.clone();
178 let slippage_percent = *slippage_percent;
179 let network_passphrase = network_passphrase.clone();
180 let token_decimals = policy.get_allowed_token_decimals(&token_asset);
181 let swap_amount_clone = *swap_amount;
182
183 Some(async move {
184 info!(
185 "Preparing swap transaction for {} tokens of type {} for relayer: {}",
186 swap_amount_clone, token_asset, relayer_id_clone
187 );
188
189 let swap_params = SwapTransactionParams {
193 source_account: relayer_address.clone(),
194 source_asset: token_asset.clone(),
195 destination_asset: "native".to_string(), amount: swap_amount_clone,
197 slippage_percent,
198 network_passphrase: network_passphrase.clone(),
199 source_asset_decimals: token_decimals,
200 destination_asset_decimals: Some(7), };
202
203 dex_service
206 .prepare_swap_transaction(swap_params)
207 .await
208 .map(|(xdr, quote)| (token_asset.clone(), swap_amount_clone, quote, xdr))
209 .map_err(|e| {
210 RelayerError::Internal(format!(
212 "Failed to prepare swap transaction for token {token_asset} (amount {swap_amount_clone}): {e}",
213 ))
214 })
215 })
216 })
217 .collect();
218
219 let swap_prep_results = join_all(swap_prep_futures).await;
223
224 let mut swap_results = Vec::new();
227 for result in swap_prep_results {
228 match result {
229 Ok((token_asset, swap_amount, quote, xdr)) => {
230 let stellar_request = StellarTransactionRequest {
232 source_account: Some(relayer.address.clone()),
233 network: relayer_network.clone(),
234 operations: None,
235 memo: None,
236 valid_until: None,
237 transaction_xdr: Some(xdr),
238 fee_bump: None,
239 max_fee: None,
240 };
241
242 let network_request = NetworkTransactionRequest::Stellar(stellar_request);
243
244 match self.process_transaction_request(network_request).await {
247 Ok(transaction_model) => {
248 info!(
249 "Swap transaction queued for relayer: {}. Token: {}, Amount: {}, Destination: {}, Transaction ID: {}",
250 relayer_id, token_asset, swap_amount, quote.out_amount, transaction_model.id
251 );
252
253 swap_results.push(SwapResult {
254 mint: token_asset,
255 source_amount: swap_amount,
256 destination_amount: quote.out_amount,
257 transaction_signature: transaction_model.id, error: None,
259 });
260 }
261 Err(e) => {
262 error!(
263 "Error queueing swap transaction for relayer: {}. Token: {}, Error: {}",
264 relayer_id, token_asset, e
265 );
266 swap_results.push(SwapResult {
267 mint: token_asset,
268 source_amount: swap_amount,
269 destination_amount: 0,
270 transaction_signature: "".to_string(),
271 error: Some(format!("Failed to queue transaction: {e}")),
272 });
273 }
274 }
275 }
276 Err(e) => {
277 error!(
280 %relayer_id,
281 error = %e,
282 "Failed to prepare swap transaction, skipping this token"
283 );
284 let error_msg = e.to_string();
287 let token_asset = error_msg
288 .split("token ")
289 .nth(1)
290 .and_then(|s| s.split(" (amount ").next())
291 .unwrap_or("unknown")
292 .to_string();
293 let swap_amount = error_msg
294 .split("(amount ")
295 .nth(1)
296 .and_then(|s| s.split(")").next())
297 .and_then(|s| s.parse::<u64>().ok())
298 .unwrap_or(0);
299
300 swap_results.push(SwapResult {
301 mint: token_asset,
302 source_amount: swap_amount,
303 destination_amount: 0,
304 transaction_signature: String::new(),
305 error: Some(error_msg),
306 });
307 }
308 }
309 }
310
311 if !swap_results.is_empty() {
312 let queued_count = swap_results
313 .iter()
314 .filter(|result| result.error.is_none())
315 .count();
316 let failed_count = swap_results.len() - queued_count;
317
318 info!(
319 "Queued {} swap transactions for relayer {} ({} successful, {} failed). \
320 Each transaction will send its own status notification when processed.",
321 swap_results.len(),
322 relayer_id,
323 queued_count,
324 failed_count
325 );
326
327 if let Some(notification_id) = &relayer.notification_id {
332 let has_queued_swaps = swap_results.iter().any(|result| {
334 result.error.is_none() && !result.transaction_signature.is_empty()
335 });
336
337 if has_queued_swaps {
338 let webhook_result = self
339 .job_producer
340 .produce_send_notification_job(
341 produce_stellar_dex_webhook_payload(
342 notification_id,
343 "stellar_dex_queued".to_string(),
344 StellarDexPayload {
345 swap_results: swap_results.clone(),
346 },
347 ),
348 None,
349 )
350 .await;
351
352 if let Err(e) = webhook_result {
353 error!(error = %e, "failed to produce swap queued notification job");
354 }
355 }
356 }
357 }
358
359 Ok(swap_results)
360 }
361}
362
363fn calculate_swap_amount(
372 current_balance: u64,
373 min_amount: Option<u64>,
374 max_amount: Option<u64>,
375 retain_min: Option<u64>,
376) -> Result<u64, RelayerError> {
377 let mut amount = max_amount
379 .map(|max| std::cmp::min(current_balance, max))
380 .unwrap_or(current_balance);
381
382 if let Some(retain) = retain_min {
384 if current_balance > retain {
385 amount = std::cmp::min(amount, current_balance - retain);
386 } else {
387 return Ok(0);
389 }
390 }
391
392 if let Some(min) = min_amount {
394 if amount < min {
395 return Ok(0); }
397 }
398
399 Ok(amount)
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use crate::{
406 config::{NetworkConfigCommon, StellarNetworkConfig},
407 domain::stellar::parse_account_id,
408 jobs::MockJobProducerTrait,
409 models::{
410 NetworkConfigData, NetworkRepoModel, NetworkType, RelayerNetworkPolicy,
411 RelayerRepoModel, RelayerStellarPolicy, RelayerStellarSwapConfig,
412 StellarAllowedTokensPolicy, StellarAllowedTokensSwapConfig, StellarFeePaymentStrategy,
413 StellarSwapStrategy,
414 },
415 repositories::{
416 InMemoryNetworkRepository, MockRelayerRepository, MockTransactionRepository,
417 },
418 services::{
419 provider::MockStellarProviderTrait, signer::MockStellarSignTrait,
420 stellar_dex::MockStellarDexServiceTrait, MockTransactionCounterServiceTrait,
421 },
422 };
423 use mockall::predicate::*;
424 use soroban_rs::xdr::{
425 AccountEntry, AccountEntryExt, AccountId, PublicKey, SequenceNumber, String32, Thresholds,
426 Uint256, VecM, WriteXdr,
427 };
428 use std::future::ready;
429 use std::sync::Arc;
430
431 const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
432 const TEST_NETWORK_PASSPHRASE: &str = "Test SDF Network ; September 2015";
433 const USDC_ASSET: &str = "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
434
435 fn create_mock_provider_with_usdc_balance(balance: i64) -> MockStellarProviderTrait {
437 let mut provider = MockStellarProviderTrait::new();
438 provider.expect_get_ledger_entries().returning(move |keys| {
439 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
440 use soroban_rs::xdr::{
441 LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey, TrustLineAsset,
442 TrustLineEntry, TrustLineEntryExt, WriteXdr,
443 };
444
445 let (account_id, asset) = if let Some(LedgerKey::Trustline(trustline_key)) =
447 keys.first()
448 {
449 (
450 trustline_key.account_id.clone(),
451 trustline_key.asset.clone(),
452 )
453 } else {
454 let fallback_account = parse_account_id(TEST_PK).unwrap_or_else(|_| {
456 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
457 });
458 let fallback_issuer =
459 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
460 .unwrap_or_else(|_| {
461 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
462 });
463 let fallback_asset = TrustLineAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
464 asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
465 issuer: fallback_issuer,
466 });
467 (fallback_account, fallback_asset)
468 };
469
470 let trustline_entry = TrustLineEntry {
471 account_id,
472 asset,
473 balance,
474 limit: i64::MAX,
475 flags: 0,
476 ext: TrustLineEntryExt::V0,
477 };
478
479 let ledger_entry = LedgerEntry {
480 last_modified_ledger_seq: 0,
481 data: LedgerEntryData::Trustline(trustline_entry),
482 ext: LedgerEntryExt::V0,
483 };
484
485 let xdr_base64 = ledger_entry
487 .data
488 .to_xdr_base64(soroban_rs::xdr::Limits::none())
489 .expect("Failed to encode trustline entry data to XDR");
490
491 Box::pin(ready(Ok(GetLedgerEntriesResponse {
492 entries: Some(vec![LedgerEntryResult {
493 key: String::new(),
494 xdr: xdr_base64,
495 last_modified_ledger: 1000,
496 live_until_ledger_seq_ledger_seq: None,
497 }]),
498 latest_ledger: 1000,
499 })))
500 });
501
502 provider.expect_get_account().returning(|_| {
504 Box::pin(ready(Ok(AccountEntry {
505 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
506 balance: 100_000_000, seq_num: SequenceNumber(100), num_sub_entries: 0,
509 inflation_dest: None,
510 flags: 0,
511 home_domain: String32::default(),
512 thresholds: Thresholds([0; 4]),
513 signers: VecM::default(),
514 ext: AccountEntryExt::V0,
515 })))
516 });
517
518 provider
519 }
520
521 fn create_test_relayer_with_swap_config() -> RelayerRepoModel {
523 let mut policy = RelayerStellarPolicy::default();
524 policy.fee_payment_strategy = Some(StellarFeePaymentStrategy::User);
525 policy.swap_config = Some(RelayerStellarSwapConfig {
526 strategies: vec![StellarSwapStrategy::OrderBook],
527 min_balance_threshold: None,
528 cron_schedule: None,
529 });
530 policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
531 asset: USDC_ASSET.to_string(),
532 metadata: None,
533 max_allowed_fee: None,
534 swap_config: Some(StellarAllowedTokensSwapConfig {
535 min_amount: Some(1000000),
536 max_amount: Some(100000000),
537 retain_min_amount: Some(1000000),
538 slippage_percentage: Some(1.0),
539 }),
540 }]);
541
542 RelayerRepoModel {
543 id: "test-relayer-id".to_string(),
544 name: "Test Relayer".to_string(),
545 network: "testnet".to_string(),
546 paused: false,
547 network_type: NetworkType::Stellar,
548 signer_id: "signer-id".to_string(),
549 policies: RelayerNetworkPolicy::Stellar(policy),
550 address: TEST_PK.to_string(),
551 notification_id: Some("notification-id".to_string()),
552 system_disabled: false,
553 custom_rpc_urls: None,
554 ..Default::default()
555 }
556 }
557
558 fn create_mock_dex_service() -> Arc<MockStellarDexServiceTrait> {
560 let mut mock_dex = MockStellarDexServiceTrait::new();
561 mock_dex.expect_supported_asset_types().returning(|| {
562 use crate::services::stellar_dex::AssetType;
563 std::collections::HashSet::from([AssetType::Native, AssetType::Classic])
564 });
565 mock_dex
566 .expect_can_handle_asset()
567 .returning(|asset| asset == USDC_ASSET || asset == "native");
568 Arc::new(mock_dex)
569 }
570
571 fn create_test_network() -> NetworkRepoModel {
573 NetworkRepoModel {
574 id: "stellar:testnet".to_string(),
575 name: "testnet".to_string(),
576 network_type: NetworkType::Stellar,
577 config: NetworkConfigData::Stellar(StellarNetworkConfig {
578 common: NetworkConfigCommon {
579 network: "testnet".to_string(),
580 from: None,
581 rpc_urls: Some(vec!["https://horizon-testnet.stellar.org".to_string()]),
582 explorer_urls: None,
583 average_blocktime_ms: Some(5000),
584 is_testnet: Some(true),
585 tags: None,
586 },
587 passphrase: Some(TEST_NETWORK_PASSPHRASE.to_string()),
588 horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
589 }),
590 }
591 }
592
593 async fn create_test_relayer_with_mocks(
595 relayer_model: RelayerRepoModel,
596 provider: MockStellarProviderTrait,
597 dex_service: Arc<MockStellarDexServiceTrait>,
598 tx_job_result: Result<(), crate::jobs::JobProducerError>,
599 notification_job_result: Result<(), crate::jobs::JobProducerError>,
600 ) -> crate::domain::relayer::stellar::StellarRelayer<
601 MockStellarProviderTrait,
602 MockRelayerRepository,
603 InMemoryNetworkRepository,
604 MockTransactionRepository,
605 MockJobProducerTrait,
606 MockTransactionCounterServiceTrait,
607 MockStellarSignTrait,
608 MockStellarDexServiceTrait,
609 > {
610 let network_repository = Arc::new(InMemoryNetworkRepository::new());
611 let test_network = create_test_network();
612 network_repository.create(test_network).await.unwrap();
613
614 let mut relayer_repo = MockRelayerRepository::new();
615 let relayer_model_clone = relayer_model.clone();
616 relayer_repo
617 .expect_get_by_id()
618 .returning(move |_| Ok(relayer_model_clone.clone()));
619
620 let relayer_model_clone2 = relayer_model.clone();
622 relayer_repo
623 .expect_update_policy()
624 .returning(move |_, _| Ok(relayer_model_clone2.clone()));
625
626 let relayer_model_clone3 = relayer_model.clone();
628 relayer_repo
629 .expect_enable_relayer()
630 .returning(move |_| Ok(relayer_model_clone3.clone()));
631 let relayer_model_clone4 = relayer_model.clone();
632 relayer_repo
633 .expect_disable_relayer()
634 .returning(move |_, _| Ok(relayer_model_clone4.clone()));
635
636 let mut tx_repo = MockTransactionRepository::new();
637 tx_repo.expect_create().returning(|t| Ok(t.clone()));
638
639 let mut job_producer = MockJobProducerTrait::new();
640 job_producer
641 .expect_produce_transaction_request_job()
642 .returning({
643 let tx_job_result = tx_job_result.clone();
644 move |_, _| {
645 let result = tx_job_result.clone();
646 Box::pin(async move { result })
647 }
648 });
649 job_producer
650 .expect_produce_send_notification_job()
651 .returning({
652 let notification_job_result = notification_job_result.clone();
653 move |_, _| {
654 let result = notification_job_result.clone();
655 Box::pin(async move { result })
656 }
657 });
658 job_producer
659 .expect_produce_relayer_health_check_job()
660 .returning(|_, _| Box::pin(async { Ok(()) }));
661 job_producer
662 .expect_produce_check_transaction_status_job()
663 .returning(|_, _| Box::pin(async { Ok(()) }));
664
665 let mut counter = MockTransactionCounterServiceTrait::new();
666 counter
667 .expect_set()
668 .returning(|_| Box::pin(async { Ok(()) }));
669 let counter = Arc::new(counter);
670 let signer = Arc::new(MockStellarSignTrait::new());
671
672 crate::domain::relayer::stellar::StellarRelayer::new(
673 relayer_model,
674 signer,
675 provider,
676 crate::domain::relayer::stellar::StellarRelayerDependencies::new(
677 Arc::new(relayer_repo),
678 network_repository,
679 Arc::new(tx_repo),
680 counter,
681 Arc::new(job_producer),
682 ),
683 dex_service,
684 )
685 .await
686 .unwrap()
687 }
688
689 async fn create_test_relayer_instance(
691 relayer_model: RelayerRepoModel,
692 provider: MockStellarProviderTrait,
693 dex_service: Arc<MockStellarDexServiceTrait>,
694 ) -> crate::domain::relayer::stellar::StellarRelayer<
695 MockStellarProviderTrait,
696 MockRelayerRepository,
697 InMemoryNetworkRepository,
698 MockTransactionRepository,
699 MockJobProducerTrait,
700 MockTransactionCounterServiceTrait,
701 MockStellarSignTrait,
702 MockStellarDexServiceTrait,
703 > {
704 create_test_relayer_with_mocks(relayer_model, provider, dex_service, Ok(()), Ok(())).await
705 }
706
707 #[tokio::test]
708 async fn test_handle_token_swap_request_with_user_fee_strategy() {
709 let relayer_model = create_test_relayer_with_swap_config();
710 let provider = create_mock_provider_with_usdc_balance(5000000); let mut dex_service = MockStellarDexServiceTrait::new();
713 dex_service.expect_supported_asset_types().returning(|| {
714 use crate::services::stellar_dex::AssetType;
715 std::collections::HashSet::from([AssetType::Native, AssetType::Classic])
716 });
717 dex_service
718 .expect_can_handle_asset()
719 .returning(|asset| asset == USDC_ASSET || asset == "native");
720
721 dex_service.expect_prepare_swap_transaction().returning(|_| {
723 Box::pin(ready(Ok((
724 "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAAEAAHAkAAAADwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=".to_string(),
725 crate::services::stellar_dex::StellarQuoteResponse {
726 input_asset: USDC_ASSET.to_string(),
727 output_asset: "native".to_string(),
728 in_amount: 40000000,
729 out_amount: 10000000,
730 price_impact_pct: 0.0,
731 slippage_bps: 100,
732 path: None,
733 },
734 ))))
735 });
736
737 let dex_service = Arc::new(dex_service);
738 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
739
740 let result = relayer
741 .handle_token_swap_request("test-relayer-id".to_string())
742 .await;
743
744 assert!(result.is_ok());
745 let swap_results = result.unwrap();
746
747 assert_eq!(swap_results.len(), 1);
749
750 let swap_result = &swap_results[0];
751
752 assert_eq!(swap_result.mint, USDC_ASSET);
754 assert_eq!(swap_result.source_amount, 4000000); assert_eq!(swap_result.destination_amount, 10000000);
756 assert!(swap_result.error.is_none());
757 assert!(!swap_result.transaction_signature.is_empty());
758
759 assert!(swap_result.transaction_signature.len() > 0);
761 }
762
763 #[tokio::test]
764 async fn test_handle_token_swap_request_with_relayer_fee_strategy() {
765 let mut relayer_model = create_test_relayer_with_swap_config();
766 if let RelayerNetworkPolicy::Stellar(ref mut policy) = relayer_model.policies {
768 policy.fee_payment_strategy = Some(StellarFeePaymentStrategy::Relayer);
769 }
770
771 let provider = MockStellarProviderTrait::new();
772 let dex_service = create_mock_dex_service();
773 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
774
775 let result = relayer
776 .handle_token_swap_request("test-relayer-id".to_string())
777 .await;
778
779 assert!(result.is_ok());
780 let swap_results = result.unwrap();
781 assert!(swap_results.is_empty());
783 }
784
785 #[tokio::test]
786 async fn test_handle_token_swap_request_no_swap_config() {
787 let mut relayer_model = create_test_relayer_with_swap_config();
788 if let RelayerNetworkPolicy::Stellar(ref mut policy) = relayer_model.policies {
790 policy.swap_config = None;
791 }
792
793 let provider = MockStellarProviderTrait::new();
794 let dex_service = create_mock_dex_service();
795 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
796
797 let result = relayer
798 .handle_token_swap_request("test-relayer-id".to_string())
799 .await;
800
801 assert!(result.is_ok());
802 let swap_results = result.unwrap();
803 assert!(swap_results.is_empty());
805 }
806
807 #[tokio::test]
808 async fn test_handle_token_swap_request_no_allowed_tokens() {
809 let mut relayer_model = create_test_relayer_with_swap_config();
810 if let RelayerNetworkPolicy::Stellar(ref mut policy) = relayer_model.policies {
812 policy.allowed_tokens = Some(vec![]);
813 }
814
815 let provider = MockStellarProviderTrait::new();
816 let dex_service = create_mock_dex_service();
817 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
818
819 let result = relayer
820 .handle_token_swap_request("test-relayer-id".to_string())
821 .await;
822
823 assert!(result.is_ok());
824 let swap_results = result.unwrap();
825 assert!(swap_results.is_empty());
827 }
828
829 #[tokio::test]
830 async fn test_handle_token_swap_request_balance_below_minimum() {
831 let relayer_model = create_test_relayer_with_swap_config();
832 let provider = create_mock_provider_with_usdc_balance(500000); let dex_service = create_mock_dex_service();
835 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
836
837 let result = relayer
838 .handle_token_swap_request("test-relayer-id".to_string())
839 .await;
840
841 assert!(result.is_ok());
842 let swap_results = result.unwrap();
843 assert!(swap_results.is_empty());
845 }
846
847 #[tokio::test]
848 async fn test_handle_token_swap_request_token_balance_fetch_failure() {
849 let relayer_model = create_test_relayer_with_swap_config();
850 let mut provider = MockStellarProviderTrait::new();
851
852 provider.expect_get_ledger_entries().returning(|_| {
854 Box::pin(ready(Err(crate::services::provider::ProviderError::Other(
855 "Connection failed".to_string(),
856 ))))
857 });
858
859 let dex_service = create_mock_dex_service();
860 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
861
862 let result = relayer
863 .handle_token_swap_request("test-relayer-id".to_string())
864 .await;
865
866 assert!(result.is_ok());
867 let swap_results = result.unwrap();
868 assert!(swap_results.is_empty());
870 }
871
872 #[tokio::test]
873 async fn test_handle_token_swap_request_dex_service_prepare_failure() {
874 let relayer_model = create_test_relayer_with_swap_config();
875 let provider = create_mock_provider_with_usdc_balance(50000000); let mut dex_service = MockStellarDexServiceTrait::new();
878 dex_service.expect_supported_asset_types().returning(|| {
879 use crate::services::stellar_dex::AssetType;
880 std::collections::HashSet::from([AssetType::Native, AssetType::Classic])
881 });
882 dex_service
883 .expect_can_handle_asset()
884 .returning(|asset| asset == USDC_ASSET || asset == "native");
885
886 dex_service
888 .expect_prepare_swap_transaction()
889 .returning(|_| {
890 Box::pin(ready(Err(
891 crate::services::stellar_dex::StellarDexServiceError::ApiError {
892 message: "Insufficient liquidity".to_string(),
893 },
894 )))
895 });
896
897 let dex_service = Arc::new(dex_service);
898 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
899
900 let result = relayer
901 .handle_token_swap_request("test-relayer-id".to_string())
902 .await;
903
904 assert!(result.is_ok());
905 let swap_results = result.unwrap();
906 assert_eq!(swap_results.len(), 1);
908 assert!(swap_results[0].error.is_some());
909 assert_eq!(swap_results[0].source_amount, 49000000); assert_eq!(swap_results[0].destination_amount, 0);
911 assert!(swap_results[0].transaction_signature.is_empty());
912 }
913
914 #[tokio::test]
915 async fn test_handle_token_swap_request_transaction_processing_failure() {
916 let relayer_model = create_test_relayer_with_swap_config();
917 let provider = create_mock_provider_with_usdc_balance(5000000); let mut dex_service = MockStellarDexServiceTrait::new();
920 dex_service.expect_supported_asset_types().returning(|| {
921 use crate::services::stellar_dex::AssetType;
922 std::collections::HashSet::from([AssetType::Native, AssetType::Classic])
923 });
924 dex_service
925 .expect_can_handle_asset()
926 .returning(|asset| asset == USDC_ASSET || asset == "native");
927
928 dex_service.expect_prepare_swap_transaction().returning(|_| {
930 Box::pin(ready(Ok((
931 "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAAEAAHAkAAAADwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=".to_string(),
932 crate::services::stellar_dex::StellarQuoteResponse {
933 input_asset: USDC_ASSET.to_string(),
934 output_asset: "native".to_string(),
935 in_amount: 40000000,
936 out_amount: 10000000,
937 price_impact_pct: 0.0,
938 slippage_bps: 100,
939 path: None,
940 },
941 ))))
942 });
943
944 let dex_service = Arc::new(dex_service);
945 let relayer = create_test_relayer_with_mocks(
946 relayer_model,
947 provider,
948 dex_service,
949 Err(crate::jobs::JobProducerError::QueueError(
950 "Queue full".to_string(),
951 )),
952 Ok(()),
953 )
954 .await;
955
956 let result = relayer
957 .handle_token_swap_request("test-relayer-id".to_string())
958 .await;
959
960 assert!(result.is_ok());
961 let swap_results = result.unwrap();
962 assert_eq!(swap_results.len(), 1);
964 assert!(swap_results[0].error.is_some());
965 assert!(swap_results[0]
966 .error
967 .as_ref()
968 .unwrap()
969 .contains("Failed to queue transaction"));
970 assert_eq!(swap_results[0].source_amount, 4000000); assert_eq!(swap_results[0].destination_amount, 0);
972 assert!(swap_results[0].transaction_signature.is_empty());
973 }
974
975 #[tokio::test]
976 async fn test_handle_token_swap_request_notification_failure() {
977 let relayer_model = create_test_relayer_with_swap_config();
978 let provider = create_mock_provider_with_usdc_balance(5000000); let mut dex_service = MockStellarDexServiceTrait::new();
981 dex_service.expect_supported_asset_types().returning(|| {
982 use crate::services::stellar_dex::AssetType;
983 std::collections::HashSet::from([AssetType::Native, AssetType::Classic])
984 });
985 dex_service
986 .expect_can_handle_asset()
987 .returning(|asset| asset == USDC_ASSET || asset == "native");
988
989 dex_service.expect_prepare_swap_transaction().returning(|_| {
991 Box::pin(ready(Ok((
992 "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAAEAAHAkAAAADwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=".to_string(),
993 crate::services::stellar_dex::StellarQuoteResponse {
994 input_asset: USDC_ASSET.to_string(),
995 output_asset: "native".to_string(),
996 in_amount: 40000000,
997 out_amount: 10000000,
998 price_impact_pct: 0.0,
999 slippage_bps: 100,
1000 path: None,
1001 },
1002 ))))
1003 });
1004
1005 let dex_service = Arc::new(dex_service);
1006 let relayer = create_test_relayer_with_mocks(
1007 relayer_model,
1008 provider,
1009 dex_service,
1010 Ok(()),
1011 Err(crate::jobs::JobProducerError::QueueError(
1012 "Notification queue full".to_string(),
1013 )),
1014 )
1015 .await;
1016
1017 let result = relayer
1018 .handle_token_swap_request("test-relayer-id".to_string())
1019 .await;
1020
1021 assert!(result.is_ok());
1023 let swap_results = result.unwrap();
1024 assert_eq!(swap_results.len(), 1);
1025 assert!(swap_results[0].error.is_none());
1026 assert!(!swap_results[0].transaction_signature.is_empty());
1027 }
1028
1029 #[tokio::test]
1030 async fn test_handle_token_swap_request_multiple_tokens() {
1031 let mut relayer_model = create_test_relayer_with_swap_config();
1032 if let RelayerNetworkPolicy::Stellar(ref mut policy) = relayer_model.policies {
1034 policy.allowed_tokens = Some(vec![
1035 StellarAllowedTokensPolicy {
1036 asset: USDC_ASSET.to_string(),
1037 metadata: None,
1038 max_allowed_fee: None,
1039 swap_config: Some(StellarAllowedTokensSwapConfig {
1040 min_amount: Some(1000000),
1041 max_amount: Some(100000000),
1042 retain_min_amount: Some(1000000),
1043 slippage_percentage: Some(1.0),
1044 }),
1045 },
1046 StellarAllowedTokensPolicy {
1047 asset: "EURC:GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2"
1048 .to_string(),
1049 metadata: None,
1050 max_allowed_fee: None,
1051 swap_config: Some(StellarAllowedTokensSwapConfig {
1052 min_amount: Some(2000000),
1053 max_amount: Some(50000000),
1054 retain_min_amount: Some(500000),
1055 slippage_percentage: Some(0.5),
1056 }),
1057 },
1058 ]);
1059 }
1060
1061 let provider = create_mock_provider_with_usdc_balance(5000000); let mut dex_service = MockStellarDexServiceTrait::new();
1065 dex_service.expect_supported_asset_types().returning(|| {
1066 use crate::services::stellar_dex::AssetType;
1067 std::collections::HashSet::from([AssetType::Native, AssetType::Classic])
1068 });
1069 dex_service.expect_can_handle_asset().returning(|asset| {
1070 asset == USDC_ASSET
1071 || asset == "EURC:GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2"
1072 || asset == "native"
1073 });
1074
1075 dex_service.expect_prepare_swap_transaction().returning(|_| {
1077 Box::pin(ready(Ok((
1078 "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAAEAAHAkAAAADwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=".to_string(),
1079 crate::services::stellar_dex::StellarQuoteResponse {
1080 input_asset: USDC_ASSET.to_string(),
1081 output_asset: "native".to_string(),
1082 in_amount: 40000000,
1083 out_amount: 10000000,
1084 price_impact_pct: 0.0,
1085 slippage_bps: 100,
1086 path: None,
1087 },
1088 ))))
1089 });
1090
1091 let dex_service = Arc::new(dex_service);
1092 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1093
1094 let result = relayer
1095 .handle_token_swap_request("test-relayer-id".to_string())
1096 .await;
1097
1098 assert!(result.is_ok());
1099 let swap_results = result.unwrap();
1100 assert_eq!(swap_results.len(), 2);
1102 assert!(swap_results.iter().all(|r| r.error.is_none()));
1103 assert!(swap_results
1104 .iter()
1105 .all(|r| !r.transaction_signature.is_empty()));
1106 }
1107
1108 #[tokio::test]
1109 async fn test_handle_token_swap_request_partial_failure() {
1110 let mut relayer_model = create_test_relayer_with_swap_config();
1111 if let RelayerNetworkPolicy::Stellar(ref mut policy) = relayer_model.policies {
1113 policy.allowed_tokens = Some(vec![
1114 StellarAllowedTokensPolicy {
1115 asset: USDC_ASSET.to_string(),
1116 metadata: None,
1117 max_allowed_fee: None,
1118 swap_config: Some(StellarAllowedTokensSwapConfig {
1119 min_amount: Some(1000000),
1120 max_amount: Some(100000000),
1121 retain_min_amount: Some(1000000),
1122 slippage_percentage: Some(1.0),
1123 }),
1124 },
1125 StellarAllowedTokensPolicy {
1126 asset: "EURC:GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2"
1127 .to_string(),
1128 metadata: None,
1129 max_allowed_fee: None,
1130 swap_config: Some(StellarAllowedTokensSwapConfig {
1131 min_amount: Some(2000000),
1132 max_amount: Some(50000000),
1133 retain_min_amount: Some(500000),
1134 slippage_percentage: Some(0.5),
1135 }),
1136 },
1137 ]);
1138 }
1139
1140 let mut provider = MockStellarProviderTrait::new();
1141
1142 let mut call_count = 0;
1144 provider.expect_get_ledger_entries().returning(move |_| {
1145 call_count += 1;
1146 if call_count == 1 {
1147 use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
1149 use soroban_rs::xdr::{
1150 LedgerEntry, LedgerEntryData, TrustLineAsset, TrustLineEntry, TrustLineEntryExt,
1151 };
1152
1153 let trustline_entry = TrustLineEntry {
1154 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1155 asset: TrustLineAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
1156 asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
1157 issuer: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([
1158 0x3b, 0x99, 0x11, 0x38, 0x0e, 0xfe, 0x98, 0x8b, 0xa0, 0xa8, 0x90, 0x0e,
1159 0xb1, 0xcf, 0xe4, 0x4f, 0x36, 0x6f, 0x7d, 0xbe, 0x94, 0x6b, 0xed, 0x07,
1160 0x72, 0x40, 0xf7, 0xf6, 0x24, 0xdf, 0x15, 0xc5,
1161 ]))),
1162 }),
1163 balance: 5000000,
1164 limit: i64::MAX,
1165 flags: 0,
1166 ext: TrustLineEntryExt::V0,
1167 };
1168
1169 let ledger_entry = LedgerEntry {
1170 last_modified_ledger_seq: 0,
1171 data: LedgerEntryData::Trustline(trustline_entry),
1172 ext: soroban_rs::xdr::LedgerEntryExt::V0,
1173 };
1174
1175 let xdr_base64 = ledger_entry
1177 .data
1178 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1179 .unwrap();
1180
1181 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1182 entries: Some(vec![LedgerEntryResult {
1183 key: String::new(),
1184 xdr: xdr_base64,
1185 last_modified_ledger: 1000,
1186 live_until_ledger_seq_ledger_seq: None,
1187 }]),
1188 latest_ledger: 1000,
1189 })))
1190 } else {
1191 Box::pin(ready(Err(crate::services::provider::ProviderError::Other(
1193 "Connection failed".to_string(),
1194 ))))
1195 }
1196 });
1197
1198 let mut dex_service = MockStellarDexServiceTrait::new();
1199 dex_service.expect_supported_asset_types().returning(|| {
1200 use crate::services::stellar_dex::AssetType;
1201 std::collections::HashSet::from([AssetType::Native, AssetType::Classic])
1202 });
1203 dex_service.expect_can_handle_asset().returning(|asset| {
1204 asset == USDC_ASSET
1205 || asset == "EURC:GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2"
1206 || asset == "native"
1207 });
1208
1209 dex_service.expect_prepare_swap_transaction().returning(|_| {
1211 Box::pin(ready(Ok((
1212 "AAAAAgAAAACige4lTdwSB/sto4SniEdJ2kOa2X65s5bqkd40J4DjSwAAAAEAAHAkAAAADwAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAKKB7iVN3BIH+y2jhKeIR0naQ5rZfrmzluqR3jQngONLAAAAAAAAAAAAD0JAAAAAAAAAAAA=".to_string(),
1213 crate::services::stellar_dex::StellarQuoteResponse {
1214 input_asset: USDC_ASSET.to_string(),
1215 output_asset: "native".to_string(),
1216 in_amount: 40000000,
1217 out_amount: 10000000,
1218 price_impact_pct: 0.0,
1219 slippage_bps: 100,
1220 path: None,
1221 },
1222 ))))
1223 });
1224
1225 let dex_service = Arc::new(dex_service);
1226 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1227
1228 let result = relayer
1229 .handle_token_swap_request("test-relayer-id".to_string())
1230 .await;
1231
1232 assert!(result.is_ok());
1233 let swap_results = result.unwrap();
1234 assert_eq!(swap_results.len(), 1);
1236 assert!(swap_results[0].error.is_none());
1237 assert!(!swap_results[0].transaction_signature.is_empty());
1238 }
1239
1240 #[test]
1241 fn test_calculate_swap_amount_no_constraints() {
1242 let result = calculate_swap_amount(10000000, None, None, None).unwrap();
1243 assert_eq!(result, 10000000);
1244 }
1245
1246 #[test]
1247 fn test_calculate_swap_amount_with_max_amount() {
1248 let result = calculate_swap_amount(10000000, None, Some(5000000), None).unwrap();
1249 assert_eq!(result, 5000000);
1250 }
1251
1252 #[test]
1253 fn test_calculate_swap_amount_with_retain_min() {
1254 let result = calculate_swap_amount(10000000, None, None, Some(2000000)).unwrap();
1255 assert_eq!(result, 8000000); }
1257
1258 #[test]
1259 fn test_calculate_swap_amount_with_max_and_retain() {
1260 let result = calculate_swap_amount(10000000, None, Some(5000000), Some(2000000)).unwrap();
1261 assert_eq!(result, 5000000); }
1263
1264 #[test]
1265 fn test_calculate_swap_amount_below_minimum() {
1266 let result = calculate_swap_amount(500000, Some(1000000), None, None).unwrap();
1267 assert_eq!(result, 0); }
1269
1270 #[test]
1271 fn test_calculate_swap_amount_insufficient_for_retain() {
1272 let result = calculate_swap_amount(1000000, None, None, Some(2000000)).unwrap();
1273 assert_eq!(result, 0); }
1275
1276 #[test]
1277 fn test_calculate_swap_amount_exact_minimum() {
1278 let result = calculate_swap_amount(1000000, Some(1000000), None, None).unwrap();
1279 assert_eq!(result, 1000000); }
1281
1282 #[test]
1283 fn test_calculate_swap_amount_all_constraints() {
1284 let result =
1289 calculate_swap_amount(10000000, Some(1000000), Some(5000000), Some(2000000)).unwrap();
1290 assert_eq!(result, 5000000);
1291 }
1292
1293 #[test]
1294 fn test_calculate_swap_amount_balance_equals_retain_min() {
1295 let result = calculate_swap_amount(2000000, None, None, Some(2000000)).unwrap();
1297 assert_eq!(result, 0);
1298 }
1299
1300 #[test]
1301 fn test_calculate_swap_amount_balance_below_retain_min() {
1302 let result = calculate_swap_amount(1000000, None, None, Some(2000000)).unwrap();
1304 assert_eq!(result, 0);
1305 }
1306
1307 #[test]
1308 fn test_calculate_swap_amount_max_amount_larger_than_available() {
1309 let result = calculate_swap_amount(10000000, None, Some(15000000), Some(2000000)).unwrap();
1311 assert_eq!(result, 8000000); }
1313
1314 #[test]
1315 fn test_calculate_swap_amount_very_large_numbers() {
1316 let large_balance = u64::MAX / 2;
1318 let large_max = u64::MAX / 4;
1319 let result = calculate_swap_amount(large_balance, None, Some(large_max), None).unwrap();
1320 assert_eq!(result, large_max); }
1322
1323 #[test]
1324 fn test_calculate_swap_amount_zero_balance() {
1325 let result = calculate_swap_amount(0, None, None, None).unwrap();
1326 assert_eq!(result, 0);
1327 }
1328
1329 #[test]
1330 fn test_calculate_swap_amount_minimum_at_boundary() {
1331 let result = calculate_swap_amount(3000000, Some(1000000), None, Some(2000000)).unwrap();
1333 assert_eq!(result, 1000000); }
1335
1336 #[test]
1337 fn test_calculate_swap_amount_max_capped_by_balance() {
1338 let result = calculate_swap_amount(5000000, None, Some(10000000), None).unwrap();
1340 assert_eq!(result, 5000000); }
1342
1343 #[test]
1344 fn test_calculate_swap_amount_complex_scenario() {
1345 let result =
1350 calculate_swap_amount(15000000, Some(2000000), Some(10000000), Some(3000000)).unwrap();
1351 assert_eq!(result, 10000000);
1352 }
1353}