1use super::{
10 AssetType, PathStep, StellarDexServiceError, StellarDexServiceTrait, StellarQuoteResponse,
11 SwapExecutionResult, SwapTransactionParams,
12};
13use crate::constants::STELLAR_DEFAULT_TRANSACTION_FEE;
14use crate::domain::relayer::string_to_muxed_account;
15use crate::domain::transaction::stellar::utils::{parse_account_id, parse_contract_address};
16use crate::services::provider::StellarProviderTrait;
17use async_trait::async_trait;
18use chrono::{Duration as ChronoDuration, Utc};
19use soroban_rs::xdr::{
20 ContractId, HostFunction, Int128Parts, InvokeContractArgs, InvokeHostFunctionOp, Limits, Memo,
21 Operation, OperationBody, Preconditions, ScAddress, ScSymbol, ScVal, ScVec, SequenceNumber,
22 TimeBounds, TimePoint, Transaction, TransactionEnvelope, TransactionExt, TransactionV1Envelope,
23 VecM, WriteXdr,
24};
25use std::collections::HashSet;
26use std::sync::Arc;
27use tracing::{debug, info, warn};
28
29const TRANSACTION_VALIDITY_MINUTES: i64 = 5;
31
32pub struct SoroswapService<P>
38where
39 P: StellarProviderTrait + Send + Sync + 'static,
40{
41 router_address: String,
43 factory_address: String,
45 native_wrapper_address: String,
47 provider: Arc<P>,
49 #[allow(dead_code)]
51 network_passphrase: String,
52}
53
54impl<P> SoroswapService<P>
55where
56 P: StellarProviderTrait + Send + Sync + 'static,
57{
58 pub fn new(
68 router_address: String,
69 factory_address: String,
70 native_wrapper_address: String,
71 provider: Arc<P>,
72 network_passphrase: String,
73 ) -> Self {
74 Self {
75 router_address,
76 factory_address,
77 native_wrapper_address,
78 provider,
79 network_passphrase,
80 }
81 }
82
83 fn parse_contract_to_sc_address(address: &str) -> Result<ScAddress, StellarDexServiceError> {
85 let hash = parse_contract_address(address).map_err(|e| {
86 StellarDexServiceError::InvalidAssetIdentifier(format!(
87 "Invalid Soroban contract address '{address}': {e}"
88 ))
89 })?;
90
91 Ok(ScAddress::Contract(ContractId(hash)))
92 }
93
94 fn build_path(
96 &self,
97 from_token: &str,
98 to_token: &str,
99 ) -> Result<ScVal, StellarDexServiceError> {
100 let from_addr = Self::parse_contract_to_sc_address(from_token)?;
101 let to_addr = Self::parse_contract_to_sc_address(to_token)?;
102
103 let path_vec: ScVec = vec![ScVal::Address(from_addr), ScVal::Address(to_addr)]
105 .try_into()
106 .map_err(|_| {
107 StellarDexServiceError::UnknownError("Failed to create path vector".to_string())
108 })?;
109
110 Ok(ScVal::Vec(Some(path_vec)))
111 }
112
113 fn i128_to_scval(amount: i128) -> ScVal {
115 let hi = (amount >> 64) as i64;
116 let lo = amount as u64;
117 ScVal::I128(Int128Parts { hi, lo })
118 }
119
120 fn scval_to_i128(val: &ScVal) -> Result<i128, StellarDexServiceError> {
122 match val {
123 ScVal::I128(parts) => {
124 let result = ((parts.hi as i128) << 64) | (parts.lo as i128);
125 Ok(result)
126 }
127 _ => Err(StellarDexServiceError::UnknownError(
128 "Expected I128 value from router".to_string(),
129 )),
130 }
131 }
132
133 fn scval_to_amounts_vec(val: &ScVal) -> Result<Vec<i128>, StellarDexServiceError> {
135 match val {
136 ScVal::Vec(Some(sc_vec)) => {
137 let mut amounts = Vec::new();
138 for item in sc_vec.iter() {
139 amounts.push(Self::scval_to_i128(item)?);
140 }
141 Ok(amounts)
142 }
143 _ => Err(StellarDexServiceError::UnknownError(
144 "Expected Vec of I128 values from router".to_string(),
145 )),
146 }
147 }
148
149 async fn call_get_amounts_out(
154 &self,
155 amount_in: i128,
156 path: ScVal,
157 ) -> Result<Vec<i128>, StellarDexServiceError> {
158 let function_name = ScSymbol::try_from("get_amounts_out").map_err(|_| {
159 StellarDexServiceError::UnknownError("Failed to create function symbol".to_string())
160 })?;
161
162 let factory_addr = Self::parse_contract_to_sc_address(&self.factory_address)?;
164 let args = vec![
165 ScVal::Address(factory_addr),
166 Self::i128_to_scval(amount_in),
167 path,
168 ];
169
170 debug!(
171 router = %self.router_address,
172 factory = %self.factory_address,
173 amount_in = amount_in,
174 "Calling Soroswap router get_amounts_out"
175 );
176
177 let result = self
178 .provider
179 .call_contract(&self.router_address, &function_name, args)
180 .await
181 .map_err(|e| StellarDexServiceError::ApiError {
182 message: format!("Soroswap router call failed: {e}"),
183 })?;
184
185 Self::scval_to_amounts_vec(&result)
186 }
187
188 fn parse_account_to_sc_address(address: &str) -> Result<ScAddress, StellarDexServiceError> {
190 let account_id = parse_account_id(address).map_err(|e| {
191 StellarDexServiceError::InvalidAssetIdentifier(format!(
192 "Invalid Stellar account address '{address}': {e}"
193 ))
194 })?;
195 Ok(ScAddress::Account(account_id))
196 }
197
198 fn build_swap_transaction_xdr(
217 &self,
218 params: &SwapTransactionParams,
219 quote: &StellarQuoteResponse,
220 ) -> Result<String, StellarDexServiceError> {
221 let source_account = string_to_muxed_account(¶ms.source_account).map_err(|e| {
223 StellarDexServiceError::InvalidAssetIdentifier(format!("Invalid source account: {e}"))
224 })?;
225
226 let to_address = Self::parse_account_to_sc_address(¶ms.source_account)?;
228
229 let out_amount = quote.out_amount as u128;
232 let slippage_bps = quote.slippage_bps as u128;
233 let basis = 10000u128;
234
235 let amount_out_min_u128 = out_amount
236 .checked_mul(basis.saturating_sub(slippage_bps))
237 .ok_or_else(|| {
238 StellarDexServiceError::UnknownError(
239 "Overflow calculating minimum output amount".to_string(),
240 )
241 })?
242 .checked_div(basis)
243 .ok_or_else(|| StellarDexServiceError::UnknownError("Division error".to_string()))?;
244
245 let amount_out_min = if amount_out_min_u128 == 0 && out_amount > 0 {
247 1i128
248 } else {
249 amount_out_min_u128 as i128
250 };
251
252 let from_token = if params.source_asset == "native" || params.source_asset.is_empty() {
254 self.native_wrapper_address.clone()
255 } else {
256 params.source_asset.clone()
257 };
258
259 let to_token =
260 if params.destination_asset == "native" || params.destination_asset.is_empty() {
261 self.native_wrapper_address.clone()
262 } else {
263 params.destination_asset.clone()
264 };
265
266 let path = self.build_path(&from_token, &to_token)?;
268
269 let now = Utc::now();
271 let deadline = now + ChronoDuration::minutes(TRANSACTION_VALIDITY_MINUTES);
272 let deadline_timestamp = deadline.timestamp() as u64;
273
274 let router_addr = Self::parse_contract_to_sc_address(&self.router_address)?;
276 let function_name = ScSymbol::try_from("swap_exact_tokens_for_tokens").map_err(|_| {
277 StellarDexServiceError::UnknownError(
278 "Failed to create swap function symbol".to_string(),
279 )
280 })?;
281
282 let args: VecM<ScVal> = vec![
283 Self::i128_to_scval(params.amount as i128), Self::i128_to_scval(amount_out_min), path, ScVal::Address(to_address), ScVal::U64(deadline_timestamp), ]
289 .try_into()
290 .map_err(|_| {
291 StellarDexServiceError::UnknownError("Failed to create swap function args".to_string())
292 })?;
293
294 let host_function = HostFunction::InvokeContract(InvokeContractArgs {
296 contract_address: router_addr,
297 function_name,
298 args,
299 });
300
301 let invoke_op = Operation {
302 source_account: None,
303 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
304 host_function,
305 auth: VecM::default(), }),
307 };
308
309 let time_bounds = TimeBounds {
311 min_time: TimePoint(0),
312 max_time: TimePoint(deadline_timestamp),
313 };
314
315 let transaction = Transaction {
317 source_account,
318 fee: STELLAR_DEFAULT_TRANSACTION_FEE,
319 seq_num: SequenceNumber(0), cond: Preconditions::Time(time_bounds),
321 memo: Memo::None,
322 operations: vec![invoke_op].try_into().map_err(|_| {
323 StellarDexServiceError::UnknownError(
324 "Failed to create operations vector".to_string(),
325 )
326 })?,
327 ext: TransactionExt::V0,
328 };
329
330 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
332 tx: transaction,
333 signatures: VecM::default(), });
335
336 envelope.to_xdr_base64(Limits::none()).map_err(|e| {
337 StellarDexServiceError::UnknownError(format!(
338 "Failed to serialize transaction to XDR: {e}"
339 ))
340 })
341 }
342}
343
344#[async_trait]
345impl<P> StellarDexServiceTrait for SoroswapService<P>
346where
347 P: StellarProviderTrait + Send + Sync + 'static,
348{
349 fn supported_asset_types(&self) -> HashSet<AssetType> {
350 HashSet::from([AssetType::Native, AssetType::Contract])
352 }
353
354 fn can_handle_asset(&self, asset_id: &str) -> bool {
355 if asset_id == "native" || asset_id.is_empty() {
357 return true;
358 }
359
360 if asset_id.starts_with('C')
362 && asset_id.len() == 56
363 && !asset_id.contains(':')
364 && stellar_strkey::Contract::from_string(asset_id).is_ok()
365 {
366 return true;
367 }
368
369 false
370 }
371
372 async fn get_token_to_xlm_quote(
373 &self,
374 asset_id: &str,
375 amount: u64,
376 slippage: f32,
377 _asset_decimals: Option<u8>,
378 ) -> Result<StellarQuoteResponse, StellarDexServiceError> {
379 if asset_id == "native" || asset_id.is_empty() {
381 return Ok(StellarQuoteResponse {
382 input_asset: "native".to_string(),
383 output_asset: "native".to_string(),
384 in_amount: amount,
385 out_amount: amount,
386 price_impact_pct: 0.0,
387 slippage_bps: (slippage * 100.0) as u32,
388 path: None,
389 });
390 }
391
392 let path = self.build_path(asset_id, &self.native_wrapper_address)?;
394
395 let amounts = self.call_get_amounts_out(amount as i128, path).await?;
397
398 let out_amount = amounts
400 .last()
401 .copied()
402 .ok_or_else(|| StellarDexServiceError::NoPathFound)?;
403
404 if out_amount <= 0 {
405 return Err(StellarDexServiceError::NoPathFound);
406 }
407
408 let out_amount_u64 = u64::try_from(out_amount).map_err(|_| {
410 StellarDexServiceError::UnknownError(format!(
411 "Output amount {out_amount} exceeds u64::MAX"
412 ))
413 })?;
414
415 debug!(
416 asset = %asset_id,
417 in_amount = amount,
418 out_amount = out_amount_u64,
419 "Soroswap quote: token -> XLM"
420 );
421
422 Ok(StellarQuoteResponse {
423 input_asset: asset_id.to_string(),
424 output_asset: "native".to_string(),
425 in_amount: amount,
426 out_amount: out_amount_u64,
427 price_impact_pct: 0.0,
428 slippage_bps: (slippage * 100.0) as u32,
429 path: Some(vec![
430 PathStep {
431 asset_code: Some(asset_id.to_string()),
432 asset_issuer: None,
433 amount,
434 },
435 PathStep {
436 asset_code: Some("native".to_string()),
437 asset_issuer: None,
438 amount: out_amount_u64,
439 },
440 ]),
441 })
442 }
443
444 async fn get_xlm_to_token_quote(
445 &self,
446 asset_id: &str,
447 amount: u64,
448 slippage: f32,
449 _asset_decimals: Option<u8>,
450 ) -> Result<StellarQuoteResponse, StellarDexServiceError> {
451 if asset_id == "native" || asset_id.is_empty() {
453 return Ok(StellarQuoteResponse {
454 input_asset: "native".to_string(),
455 output_asset: "native".to_string(),
456 in_amount: amount,
457 out_amount: amount,
458 price_impact_pct: 0.0,
459 slippage_bps: (slippage * 100.0) as u32,
460 path: None,
461 });
462 }
463
464 let path = self.build_path(&self.native_wrapper_address, asset_id)?;
466
467 let amounts = self.call_get_amounts_out(amount as i128, path).await?;
469
470 let out_amount = amounts
472 .last()
473 .copied()
474 .ok_or_else(|| StellarDexServiceError::NoPathFound)?;
475
476 if out_amount <= 0 {
477 return Err(StellarDexServiceError::NoPathFound);
478 }
479
480 let out_amount_u64 = u64::try_from(out_amount).map_err(|_| {
482 StellarDexServiceError::UnknownError(format!(
483 "Output amount {out_amount} exceeds u64::MAX"
484 ))
485 })?;
486
487 let price_impact = if amount > 0 && out_amount_u64 > 0 {
490 let expected_ratio = 1.0;
491 let actual_ratio = out_amount_u64 as f64 / amount as f64;
492 ((expected_ratio - actual_ratio).abs() / expected_ratio * 100.0).min(100.0)
493 } else {
494 0.0
495 };
496
497 debug!(
498 asset = %asset_id,
499 in_amount = amount,
500 out_amount = out_amount_u64,
501 "Soroswap quote: XLM -> token"
502 );
503
504 Ok(StellarQuoteResponse {
505 input_asset: "native".to_string(),
506 output_asset: asset_id.to_string(),
507 in_amount: amount,
508 out_amount: out_amount_u64,
509 price_impact_pct: price_impact,
510 slippage_bps: (slippage * 100.0) as u32,
511 path: Some(vec![
512 PathStep {
513 asset_code: Some("native".to_string()),
514 asset_issuer: None,
515 amount,
516 },
517 PathStep {
518 asset_code: Some(asset_id.to_string()),
519 asset_issuer: None,
520 amount: out_amount_u64,
521 },
522 ]),
523 })
524 }
525
526 async fn prepare_swap_transaction(
527 &self,
528 params: SwapTransactionParams,
529 ) -> Result<(String, StellarQuoteResponse), StellarDexServiceError> {
530 let quote = if params.destination_asset == "native" {
532 self.get_token_to_xlm_quote(
533 ¶ms.source_asset,
534 params.amount,
535 params.slippage_percent,
536 params.source_asset_decimals,
537 )
538 .await?
539 } else if params.source_asset == "native" {
540 self.get_xlm_to_token_quote(
541 ¶ms.destination_asset,
542 params.amount,
543 params.slippage_percent,
544 params.destination_asset_decimals,
545 )
546 .await?
547 } else {
548 return Err(StellarDexServiceError::InvalidAssetIdentifier(
549 "Soroswap currently only supports swaps involving native XLM".to_string(),
550 ));
551 };
552
553 info!(
554 "Preparing Soroswap swap transaction: {} {} -> {} (min receive: {})",
555 params.amount, params.source_asset, params.destination_asset, quote.out_amount
556 );
557
558 let xdr = self.build_swap_transaction_xdr(¶ms, "e)?;
560
561 info!(
562 "Successfully prepared Soroswap swap transaction XDR ({} bytes)",
563 xdr.len()
564 );
565
566 Ok((xdr, quote))
567 }
568
569 async fn execute_swap(
574 &self,
575 _params: SwapTransactionParams,
576 ) -> Result<SwapExecutionResult, StellarDexServiceError> {
577 warn!("Soroswap execute_swap is not yet implemented");
578
579 Err(StellarDexServiceError::UnknownError(
580 "Soroswap swap execution is not yet implemented".to_string(),
581 ))
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588 use crate::constants::STELLAR_SOROSWAP_MAINNET_NATIVE_WRAPPER;
589 use crate::services::provider::MockStellarProviderTrait;
590 use futures::FutureExt;
591 use soroban_rs::xdr::ReadXdr;
592
593 const TEST_NATIVE_WRAPPER: &str = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
594
595 fn create_mock_provider() -> Arc<MockStellarProviderTrait> {
596 Arc::new(MockStellarProviderTrait::new())
597 }
598
599 fn create_test_service(
600 provider: Arc<MockStellarProviderTrait>,
601 ) -> SoroswapService<MockStellarProviderTrait> {
602 SoroswapService::new(
603 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(), "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA".to_string(), TEST_NATIVE_WRAPPER.to_string(),
606 provider,
607 "Test SDF Network ; September 2015".to_string(),
608 )
609 }
610
611 #[test]
614 fn test_new_stores_provided_native_wrapper() {
615 let provider = create_mock_provider();
616 let service = create_test_service(provider);
617 assert_eq!(service.native_wrapper_address, TEST_NATIVE_WRAPPER);
618 }
619
620 #[test]
621 fn test_new_with_mainnet_native_wrapper() {
622 let provider = create_mock_provider();
623 let service = SoroswapService::new(
624 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
625 "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA".to_string(),
626 STELLAR_SOROSWAP_MAINNET_NATIVE_WRAPPER.to_string(),
627 provider,
628 "Public Global Stellar Network ; September 2015".to_string(),
629 );
630 assert_eq!(
631 service.native_wrapper_address,
632 STELLAR_SOROSWAP_MAINNET_NATIVE_WRAPPER
633 );
634 }
635
636 #[test]
637 fn test_new_with_custom_native_wrapper() {
638 let provider = create_mock_provider();
639 let custom_wrapper = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M".to_string();
640 let service = SoroswapService::new(
641 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
642 "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA".to_string(),
643 custom_wrapper.clone(),
644 provider,
645 "Test SDF Network ; September 2015".to_string(),
646 );
647 assert_eq!(service.native_wrapper_address, custom_wrapper);
648 }
649
650 #[test]
653 fn test_parse_contract_to_sc_address_valid() {
654 let addr = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
655 let result =
656 SoroswapService::<MockStellarProviderTrait>::parse_contract_to_sc_address(addr);
657 assert!(result.is_ok());
658 match result.unwrap() {
659 ScAddress::Contract(_) => {}
660 _ => panic!("Expected Contract address"),
661 }
662 }
663
664 #[test]
665 fn test_parse_contract_to_sc_address_invalid_format() {
666 let addr = "INVALID_ADDRESS";
667 let result =
668 SoroswapService::<MockStellarProviderTrait>::parse_contract_to_sc_address(addr);
669 assert!(result.is_err());
670 match result.unwrap_err() {
671 StellarDexServiceError::InvalidAssetIdentifier(msg) => {
672 assert!(msg.contains("Invalid Soroban contract address"));
673 }
674 _ => panic!("Expected InvalidAssetIdentifier error"),
675 }
676 }
677
678 #[test]
679 fn test_parse_contract_to_sc_address_stellar_account_not_contract() {
680 let addr = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
682 let result =
683 SoroswapService::<MockStellarProviderTrait>::parse_contract_to_sc_address(addr);
684 assert!(result.is_err());
685 }
686
687 #[test]
690 fn test_can_handle_asset_native() {
691 let provider = create_mock_provider();
692 let service = create_test_service(provider);
693 assert!(service.can_handle_asset("native"));
694 }
695
696 #[test]
697 fn test_can_handle_asset_empty_string() {
698 let provider = create_mock_provider();
699 let service = create_test_service(provider);
700 assert!(service.can_handle_asset(""));
701 }
702
703 #[test]
704 fn test_can_handle_asset_valid_contract() {
705 let provider = create_mock_provider();
706 let service = create_test_service(provider);
707 let contract_addr = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
708 assert!(service.can_handle_asset(contract_addr));
709 }
710
711 #[test]
712 fn test_cannot_handle_classic_asset() {
713 let provider = create_mock_provider();
714 let service = create_test_service(provider);
715 let classic_asset = "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
716 assert!(!service.can_handle_asset(classic_asset));
717 }
718
719 #[test]
720 fn test_cannot_handle_short_address() {
721 let provider = create_mock_provider();
722 let service = create_test_service(provider);
723 assert!(!service.can_handle_asset("CSHORT"));
724 }
725
726 #[test]
727 fn test_cannot_handle_non_c_prefix() {
728 let provider = create_mock_provider();
729 let service = create_test_service(provider);
730 let addr = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
732 assert!(!service.can_handle_asset(addr));
733 }
734
735 #[test]
736 fn test_cannot_handle_invalid_contract_checksum() {
737 let provider = create_mock_provider();
738 let service = create_test_service(provider);
739 let invalid_addr = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
741 assert!(!service.can_handle_asset(invalid_addr));
742 }
743
744 #[test]
747 fn test_supported_asset_types() {
748 let provider = create_mock_provider();
749 let service = create_test_service(provider);
750 let types = service.supported_asset_types();
751 assert!(types.contains(&AssetType::Native));
752 assert!(types.contains(&AssetType::Contract));
753 assert_eq!(types.len(), 2);
754 }
755
756 #[test]
759 fn test_i128_to_scval_and_back_positive() {
760 let original: i128 = 1_000_000_000;
761 let scval = SoroswapService::<MockStellarProviderTrait>::i128_to_scval(original);
762 let recovered = SoroswapService::<MockStellarProviderTrait>::scval_to_i128(&scval).unwrap();
763 assert_eq!(original, recovered);
764 }
765
766 #[test]
767 fn test_i128_to_scval_and_back_zero() {
768 let original: i128 = 0;
769 let scval = SoroswapService::<MockStellarProviderTrait>::i128_to_scval(original);
770 let recovered = SoroswapService::<MockStellarProviderTrait>::scval_to_i128(&scval).unwrap();
771 assert_eq!(original, recovered);
772 }
773
774 #[test]
775 fn test_i128_to_scval_and_back_negative() {
776 let original: i128 = -1_000_000_000;
777 let scval = SoroswapService::<MockStellarProviderTrait>::i128_to_scval(original);
778 let recovered = SoroswapService::<MockStellarProviderTrait>::scval_to_i128(&scval).unwrap();
779 assert_eq!(original, recovered);
780 }
781
782 #[test]
783 fn test_i128_to_scval_and_back_large_positive() {
784 let original: i128 = i128::MAX / 2;
785 let scval = SoroswapService::<MockStellarProviderTrait>::i128_to_scval(original);
786 let recovered = SoroswapService::<MockStellarProviderTrait>::scval_to_i128(&scval).unwrap();
787 assert_eq!(original, recovered);
788 }
789
790 #[test]
791 fn test_i128_to_scval_and_back_large_negative() {
792 let original: i128 = i128::MIN / 2;
793 let scval = SoroswapService::<MockStellarProviderTrait>::i128_to_scval(original);
794 let recovered = SoroswapService::<MockStellarProviderTrait>::scval_to_i128(&scval).unwrap();
795 assert_eq!(original, recovered);
796 }
797
798 #[test]
799 fn test_scval_to_i128_wrong_type() {
800 let scval = ScVal::Bool(true);
801 let result = SoroswapService::<MockStellarProviderTrait>::scval_to_i128(&scval);
802 assert!(result.is_err());
803 match result.unwrap_err() {
804 StellarDexServiceError::UnknownError(msg) => {
805 assert!(msg.contains("Expected I128 value"));
806 }
807 _ => panic!("Expected UnknownError"),
808 }
809 }
810
811 #[test]
814 fn test_scval_to_amounts_vec_valid() {
815 let amounts: Vec<i128> = vec![100, 200, 300];
816 let sc_vals: Vec<ScVal> = amounts
817 .iter()
818 .map(|&a| SoroswapService::<MockStellarProviderTrait>::i128_to_scval(a))
819 .collect();
820 let sc_vec: ScVec = sc_vals.try_into().unwrap();
821 let scval = ScVal::Vec(Some(sc_vec));
822
823 let result =
824 SoroswapService::<MockStellarProviderTrait>::scval_to_amounts_vec(&scval).unwrap();
825 assert_eq!(result, vec![100, 200, 300]);
826 }
827
828 #[test]
829 fn test_scval_to_amounts_vec_empty() {
830 let sc_vec: ScVec = vec![].try_into().unwrap();
831 let scval = ScVal::Vec(Some(sc_vec));
832
833 let result =
834 SoroswapService::<MockStellarProviderTrait>::scval_to_amounts_vec(&scval).unwrap();
835 assert!(result.is_empty());
836 }
837
838 #[test]
839 fn test_scval_to_amounts_vec_wrong_type() {
840 let scval = ScVal::Bool(true);
841 let result = SoroswapService::<MockStellarProviderTrait>::scval_to_amounts_vec(&scval);
842 assert!(result.is_err());
843 match result.unwrap_err() {
844 StellarDexServiceError::UnknownError(msg) => {
845 assert!(msg.contains("Expected Vec of I128 values"));
846 }
847 _ => panic!("Expected UnknownError"),
848 }
849 }
850
851 #[test]
852 fn test_scval_to_amounts_vec_none() {
853 let scval = ScVal::Vec(None);
854 let result = SoroswapService::<MockStellarProviderTrait>::scval_to_amounts_vec(&scval);
855 assert!(result.is_err());
856 }
857
858 #[test]
859 fn test_scval_to_amounts_vec_mixed_types() {
860 let sc_vec: ScVec = vec![ScVal::Bool(true)].try_into().unwrap();
862 let scval = ScVal::Vec(Some(sc_vec));
863
864 let result = SoroswapService::<MockStellarProviderTrait>::scval_to_amounts_vec(&scval);
865 assert!(result.is_err());
866 }
867
868 #[test]
871 fn test_build_path_valid() {
872 let provider = create_mock_provider();
873 let service = create_test_service(provider);
874 let from = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
875 let to = "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA";
876
877 let result = service.build_path(from, to);
878 assert!(result.is_ok());
879 match result.unwrap() {
880 ScVal::Vec(Some(vec)) => {
881 assert_eq!(vec.len(), 2);
882 }
883 _ => panic!("Expected Vec"),
884 }
885 }
886
887 #[test]
888 fn test_build_path_invalid_from() {
889 let provider = create_mock_provider();
890 let service = create_test_service(provider);
891 let result = service.build_path(
892 "INVALID",
893 "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA",
894 );
895 assert!(result.is_err());
896 }
897
898 #[test]
899 fn test_build_path_invalid_to() {
900 let provider = create_mock_provider();
901 let service = create_test_service(provider);
902 let result = service.build_path(
903 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
904 "INVALID",
905 );
906 assert!(result.is_err());
907 }
908
909 #[tokio::test]
912 async fn test_get_token_to_xlm_quote_native_returns_1_to_1() {
913 let provider = create_mock_provider();
914 let service = create_test_service(provider);
915
916 let quote = service
917 .get_token_to_xlm_quote("native", 1_000_000, 0.5, None)
918 .await
919 .unwrap();
920
921 assert_eq!(quote.input_asset, "native");
922 assert_eq!(quote.output_asset, "native");
923 assert_eq!(quote.in_amount, 1_000_000);
924 assert_eq!(quote.out_amount, 1_000_000);
925 assert_eq!(quote.price_impact_pct, 0.0);
926 assert_eq!(quote.slippage_bps, 50);
927 assert!(quote.path.is_none());
928 }
929
930 #[tokio::test]
931 async fn test_get_token_to_xlm_quote_empty_returns_1_to_1() {
932 let provider = create_mock_provider();
933 let service = create_test_service(provider);
934
935 let quote = service
936 .get_token_to_xlm_quote("", 1_000_000, 1.0, None)
937 .await
938 .unwrap();
939
940 assert_eq!(quote.input_asset, "native");
941 assert_eq!(quote.output_asset, "native");
942 assert_eq!(quote.in_amount, quote.out_amount);
943 }
944
945 #[tokio::test]
946 async fn test_get_xlm_to_token_quote_native_returns_1_to_1() {
947 let provider = create_mock_provider();
948 let service = create_test_service(provider);
949
950 let quote = service
951 .get_xlm_to_token_quote("native", 1_000_000, 0.5, None)
952 .await
953 .unwrap();
954
955 assert_eq!(quote.input_asset, "native");
956 assert_eq!(quote.output_asset, "native");
957 assert_eq!(quote.in_amount, 1_000_000);
958 assert_eq!(quote.out_amount, 1_000_000);
959 }
960
961 #[tokio::test]
962 async fn test_get_xlm_to_token_quote_empty_returns_1_to_1() {
963 let provider = create_mock_provider();
964 let service = create_test_service(provider);
965
966 let quote = service
967 .get_xlm_to_token_quote("", 500_000, 0.25, None)
968 .await
969 .unwrap();
970
971 assert_eq!(quote.input_asset, "native");
972 assert_eq!(quote.output_asset, "native");
973 assert_eq!(quote.slippage_bps, 25);
974 }
975
976 #[tokio::test]
977 async fn test_get_token_to_xlm_quote_with_mock_provider() {
978 let mut mock = MockStellarProviderTrait::new();
979
980 let amounts: Vec<i128> = vec![1_000_000, 950_000];
982 let sc_vals: Vec<ScVal> = amounts
983 .iter()
984 .map(|&a| SoroswapService::<MockStellarProviderTrait>::i128_to_scval(a))
985 .collect();
986 let sc_vec: ScVec = sc_vals.try_into().unwrap();
987 let result_scval = ScVal::Vec(Some(sc_vec));
988
989 mock.expect_call_contract().returning(move |_, _, _| {
990 let result = result_scval.clone();
991 async move { Ok(result) }.boxed()
992 });
993
994 let provider = Arc::new(mock);
995 let service = create_test_service(provider);
996
997 let quote = service
998 .get_token_to_xlm_quote(
999 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1000 1_000_000,
1001 0.5,
1002 None,
1003 )
1004 .await
1005 .unwrap();
1006
1007 assert_eq!(quote.in_amount, 1_000_000);
1008 assert_eq!(quote.out_amount, 950_000);
1009 assert_eq!(quote.output_asset, "native");
1010 assert!(quote.path.is_some());
1011 assert_eq!(quote.path.as_ref().unwrap().len(), 2);
1012 }
1013
1014 #[tokio::test]
1015 async fn test_get_xlm_to_token_quote_with_mock_provider() {
1016 let mut mock = MockStellarProviderTrait::new();
1017
1018 let amounts: Vec<i128> = vec![1_000_000, 1_050_000];
1019 let sc_vals: Vec<ScVal> = amounts
1020 .iter()
1021 .map(|&a| SoroswapService::<MockStellarProviderTrait>::i128_to_scval(a))
1022 .collect();
1023 let sc_vec: ScVec = sc_vals.try_into().unwrap();
1024 let result_scval = ScVal::Vec(Some(sc_vec));
1025
1026 mock.expect_call_contract().returning(move |_, _, _| {
1027 let result = result_scval.clone();
1028 async move { Ok(result) }.boxed()
1029 });
1030
1031 let provider = Arc::new(mock);
1032 let service = create_test_service(provider);
1033
1034 let quote = service
1035 .get_xlm_to_token_quote(
1036 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1037 1_000_000,
1038 0.5,
1039 None,
1040 )
1041 .await
1042 .unwrap();
1043
1044 assert_eq!(quote.in_amount, 1_000_000);
1045 assert_eq!(quote.out_amount, 1_050_000);
1046 assert_eq!(quote.input_asset, "native");
1047 }
1048
1049 #[tokio::test]
1050 async fn test_get_token_to_xlm_quote_empty_amounts_returns_no_path() {
1051 let mut mock = MockStellarProviderTrait::new();
1052
1053 let sc_vec: ScVec = vec![].try_into().unwrap();
1055 let result_scval = ScVal::Vec(Some(sc_vec));
1056
1057 mock.expect_call_contract().returning(move |_, _, _| {
1058 let result = result_scval.clone();
1059 async move { Ok(result) }.boxed()
1060 });
1061
1062 let provider = Arc::new(mock);
1063 let service = create_test_service(provider);
1064
1065 let result = service
1066 .get_token_to_xlm_quote(
1067 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1068 1_000_000,
1069 0.5,
1070 None,
1071 )
1072 .await;
1073
1074 assert!(result.is_err());
1075 match result.unwrap_err() {
1076 StellarDexServiceError::NoPathFound => {}
1077 e => panic!("Expected NoPathFound error, got {:?}", e),
1078 }
1079 }
1080
1081 #[tokio::test]
1082 async fn test_get_token_to_xlm_quote_zero_output_returns_no_path() {
1083 let mut mock = MockStellarProviderTrait::new();
1084
1085 let amounts: Vec<i128> = vec![1_000_000, 0];
1086 let sc_vals: Vec<ScVal> = amounts
1087 .iter()
1088 .map(|&a| SoroswapService::<MockStellarProviderTrait>::i128_to_scval(a))
1089 .collect();
1090 let sc_vec: ScVec = sc_vals.try_into().unwrap();
1091 let result_scval = ScVal::Vec(Some(sc_vec));
1092
1093 mock.expect_call_contract().returning(move |_, _, _| {
1094 let result = result_scval.clone();
1095 async move { Ok(result) }.boxed()
1096 });
1097
1098 let provider = Arc::new(mock);
1099 let service = create_test_service(provider);
1100
1101 let result = service
1102 .get_token_to_xlm_quote(
1103 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1104 1_000_000,
1105 0.5,
1106 None,
1107 )
1108 .await;
1109
1110 assert!(result.is_err());
1111 match result.unwrap_err() {
1112 StellarDexServiceError::NoPathFound => {}
1113 e => panic!("Expected NoPathFound error, got {:?}", e),
1114 }
1115 }
1116
1117 #[tokio::test]
1118 async fn test_get_token_to_xlm_quote_negative_output_returns_no_path() {
1119 let mut mock = MockStellarProviderTrait::new();
1120
1121 let amounts: Vec<i128> = vec![1_000_000, -100];
1122 let sc_vals: Vec<ScVal> = amounts
1123 .iter()
1124 .map(|&a| SoroswapService::<MockStellarProviderTrait>::i128_to_scval(a))
1125 .collect();
1126 let sc_vec: ScVec = sc_vals.try_into().unwrap();
1127 let result_scval = ScVal::Vec(Some(sc_vec));
1128
1129 mock.expect_call_contract().returning(move |_, _, _| {
1130 let result = result_scval.clone();
1131 async move { Ok(result) }.boxed()
1132 });
1133
1134 let provider = Arc::new(mock);
1135 let service = create_test_service(provider);
1136
1137 let result = service
1138 .get_token_to_xlm_quote(
1139 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1140 1_000_000,
1141 0.5,
1142 None,
1143 )
1144 .await;
1145
1146 assert!(result.is_err());
1147 match result.unwrap_err() {
1148 StellarDexServiceError::NoPathFound => {}
1149 e => panic!("Expected NoPathFound error, got {:?}", e),
1150 }
1151 }
1152
1153 #[tokio::test]
1154 async fn test_get_token_to_xlm_quote_provider_error() {
1155 let mut mock = MockStellarProviderTrait::new();
1156
1157 mock.expect_call_contract().returning(|_, _, _| {
1158 async move {
1159 Err(crate::services::provider::ProviderError::Other(
1160 "Connection failed".to_string(),
1161 ))
1162 }
1163 .boxed()
1164 });
1165
1166 let provider = Arc::new(mock);
1167 let service = create_test_service(provider);
1168
1169 let result = service
1170 .get_token_to_xlm_quote(
1171 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1172 1_000_000,
1173 0.5,
1174 None,
1175 )
1176 .await;
1177
1178 assert!(result.is_err());
1179 match result.unwrap_err() {
1180 StellarDexServiceError::ApiError { message } => {
1181 assert!(message.contains("router call failed"));
1182 }
1183 e => panic!("Expected ApiError, got {:?}", e),
1184 }
1185 }
1186
1187 #[tokio::test]
1190 async fn test_prepare_swap_transaction_token_to_native() {
1191 let mut mock = MockStellarProviderTrait::new();
1192
1193 let amounts: Vec<i128> = vec![1_000_000, 950_000];
1194 let sc_vals: Vec<ScVal> = amounts
1195 .iter()
1196 .map(|&a| SoroswapService::<MockStellarProviderTrait>::i128_to_scval(a))
1197 .collect();
1198 let sc_vec: ScVec = sc_vals.try_into().unwrap();
1199 let result_scval = ScVal::Vec(Some(sc_vec));
1200
1201 mock.expect_call_contract().returning(move |_, _, _| {
1202 let result = result_scval.clone();
1203 async move { Ok(result) }.boxed()
1204 });
1205
1206 let provider = Arc::new(mock);
1207 let service = create_test_service(provider);
1208
1209 let params = SwapTransactionParams {
1210 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1211 source_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
1212 destination_asset: "native".to_string(),
1213 amount: 1_000_000,
1214 slippage_percent: 0.5,
1215 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1216 source_asset_decimals: Some(7),
1217 destination_asset_decimals: None,
1218 };
1219
1220 let (xdr, quote) = service.prepare_swap_transaction(params).await.unwrap();
1221
1222 assert!(!xdr.is_empty());
1223 assert_eq!(quote.out_amount, 950_000);
1224
1225 let envelope = TransactionEnvelope::from_xdr_base64(&xdr, Limits::none()).unwrap();
1227 match &envelope {
1228 TransactionEnvelope::Tx(env) => {
1229 assert_eq!(env.tx.operations.len(), 1);
1230 assert!(matches!(
1231 env.tx.operations[0].body,
1232 OperationBody::InvokeHostFunction(_)
1233 ));
1234 assert_eq!(env.tx.seq_num.0, 0);
1236 }
1237 _ => panic!("Expected Tx envelope"),
1238 }
1239 }
1240
1241 #[tokio::test]
1242 async fn test_prepare_swap_transaction_native_to_token() {
1243 let mut mock = MockStellarProviderTrait::new();
1244
1245 let amounts: Vec<i128> = vec![1_000_000, 1_050_000];
1246 let sc_vals: Vec<ScVal> = amounts
1247 .iter()
1248 .map(|&a| SoroswapService::<MockStellarProviderTrait>::i128_to_scval(a))
1249 .collect();
1250 let sc_vec: ScVec = sc_vals.try_into().unwrap();
1251 let result_scval = ScVal::Vec(Some(sc_vec));
1252
1253 mock.expect_call_contract().returning(move |_, _, _| {
1254 let result = result_scval.clone();
1255 async move { Ok(result) }.boxed()
1256 });
1257
1258 let provider = Arc::new(mock);
1259 let service = create_test_service(provider);
1260
1261 let params = SwapTransactionParams {
1262 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1263 source_asset: "native".to_string(),
1264 destination_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
1265 .to_string(),
1266 amount: 1_000_000,
1267 slippage_percent: 0.5,
1268 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1269 source_asset_decimals: None,
1270 destination_asset_decimals: Some(7),
1271 };
1272
1273 let (xdr, quote) = service.prepare_swap_transaction(params).await.unwrap();
1274
1275 assert!(!xdr.is_empty());
1276 assert_eq!(quote.out_amount, 1_050_000);
1277
1278 let envelope = TransactionEnvelope::from_xdr_base64(&xdr, Limits::none()).unwrap();
1280 match &envelope {
1281 TransactionEnvelope::Tx(env) => {
1282 assert_eq!(env.tx.operations.len(), 1);
1283 assert!(matches!(
1284 env.tx.operations[0].body,
1285 OperationBody::InvokeHostFunction(_)
1286 ));
1287 }
1288 _ => panic!("Expected Tx envelope"),
1289 }
1290 }
1291
1292 #[tokio::test]
1293 async fn test_prepare_swap_transaction_token_to_token_not_supported() {
1294 let provider = create_mock_provider();
1295 let service = create_test_service(provider);
1296
1297 let params = SwapTransactionParams {
1298 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1299 source_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
1300 destination_asset: "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
1301 .to_string(),
1302 amount: 1_000_000,
1303 slippage_percent: 0.5,
1304 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1305 source_asset_decimals: Some(7),
1306 destination_asset_decimals: Some(7),
1307 };
1308
1309 let result = service.prepare_swap_transaction(params).await;
1310
1311 assert!(result.is_err());
1312 match result.unwrap_err() {
1313 StellarDexServiceError::InvalidAssetIdentifier(msg) => {
1314 assert!(msg.contains("only supports swaps involving native XLM"));
1315 }
1316 e => panic!("Expected InvalidAssetIdentifier, got {:?}", e),
1317 }
1318 }
1319
1320 #[test]
1323 fn test_parse_account_to_sc_address_valid() {
1324 let addr = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
1325 let result = SoroswapService::<MockStellarProviderTrait>::parse_account_to_sc_address(addr);
1326 assert!(result.is_ok());
1327 match result.unwrap() {
1328 ScAddress::Account(_) => {}
1329 _ => panic!("Expected Account address"),
1330 }
1331 }
1332
1333 #[test]
1334 fn test_parse_account_to_sc_address_invalid() {
1335 let addr = "INVALID";
1336 let result = SoroswapService::<MockStellarProviderTrait>::parse_account_to_sc_address(addr);
1337 assert!(result.is_err());
1338 }
1339
1340 #[test]
1343 fn test_build_swap_transaction_xdr_token_to_xlm() {
1344 let provider = create_mock_provider();
1345 let service = create_test_service(provider);
1346
1347 let quote = StellarQuoteResponse {
1348 input_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
1349 output_asset: "native".to_string(),
1350 in_amount: 1_000_000,
1351 out_amount: 950_000,
1352 price_impact_pct: 0.0,
1353 slippage_bps: 50,
1354 path: None,
1355 };
1356
1357 let params = SwapTransactionParams {
1358 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1359 source_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
1360 destination_asset: "native".to_string(),
1361 amount: 1_000_000,
1362 slippage_percent: 0.5,
1363 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1364 source_asset_decimals: Some(7),
1365 destination_asset_decimals: None,
1366 };
1367
1368 let xdr = service.build_swap_transaction_xdr(¶ms, "e).unwrap();
1369
1370 let envelope = TransactionEnvelope::from_xdr_base64(&xdr, Limits::none()).unwrap();
1372 match &envelope {
1373 TransactionEnvelope::Tx(env) => {
1374 assert_eq!(env.tx.operations.len(), 1);
1376 assert_eq!(env.tx.seq_num.0, 0); assert_eq!(env.tx.fee, STELLAR_DEFAULT_TRANSACTION_FEE);
1378 assert!(env.signatures.is_empty()); match &env.tx.operations[0].body {
1382 OperationBody::InvokeHostFunction(op) => {
1383 match &op.host_function {
1384 HostFunction::InvokeContract(args) => {
1385 match &args.contract_address {
1387 ScAddress::Contract(_) => {}
1388 _ => panic!("Expected Contract address for router"),
1389 }
1390 assert_eq!(
1392 args.function_name.to_string(),
1393 "swap_exact_tokens_for_tokens"
1394 );
1395 assert_eq!(args.args.len(), 5);
1397 }
1398 _ => panic!("Expected InvokeContract"),
1399 }
1400 assert!(op.auth.is_empty());
1402 }
1403 _ => panic!("Expected InvokeHostFunction"),
1404 }
1405
1406 match &env.tx.cond {
1408 Preconditions::Time(tb) => {
1409 assert_eq!(tb.min_time.0, 0);
1410 assert!(tb.max_time.0 > 0);
1411 }
1412 _ => panic!("Expected Time preconditions"),
1413 }
1414 }
1415 _ => panic!("Expected Tx envelope"),
1416 }
1417 }
1418
1419 #[test]
1420 fn test_build_swap_transaction_xdr_native_to_token() {
1421 let provider = create_mock_provider();
1422 let service = create_test_service(provider);
1423
1424 let quote = StellarQuoteResponse {
1425 input_asset: "native".to_string(),
1426 output_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
1427 in_amount: 1_000_000,
1428 out_amount: 1_050_000,
1429 price_impact_pct: 0.0,
1430 slippage_bps: 50,
1431 path: None,
1432 };
1433
1434 let params = SwapTransactionParams {
1435 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1436 source_asset: "native".to_string(),
1437 destination_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
1438 .to_string(),
1439 amount: 1_000_000,
1440 slippage_percent: 0.5,
1441 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1442 source_asset_decimals: None,
1443 destination_asset_decimals: Some(7),
1444 };
1445
1446 let xdr = service.build_swap_transaction_xdr(¶ms, "e).unwrap();
1447
1448 let envelope = TransactionEnvelope::from_xdr_base64(&xdr, Limits::none()).unwrap();
1450 match &envelope {
1451 TransactionEnvelope::Tx(env) => {
1452 assert_eq!(env.tx.operations.len(), 1);
1453 match &env.tx.operations[0].body {
1454 OperationBody::InvokeHostFunction(op) => match &op.host_function {
1455 HostFunction::InvokeContract(args) => {
1456 assert_eq!(
1457 args.function_name.to_string(),
1458 "swap_exact_tokens_for_tokens"
1459 );
1460 assert_eq!(args.args.len(), 5);
1461 }
1462 _ => panic!("Expected InvokeContract"),
1463 },
1464 _ => panic!("Expected InvokeHostFunction"),
1465 }
1466 }
1467 _ => panic!("Expected Tx envelope"),
1468 }
1469 }
1470
1471 #[test]
1472 fn test_build_swap_transaction_xdr_invalid_source_account() {
1473 let provider = create_mock_provider();
1474 let service = create_test_service(provider);
1475
1476 let quote = StellarQuoteResponse {
1477 input_asset: "native".to_string(),
1478 output_asset: "native".to_string(),
1479 in_amount: 1_000_000,
1480 out_amount: 1_000_000,
1481 price_impact_pct: 0.0,
1482 slippage_bps: 50,
1483 path: None,
1484 };
1485
1486 let params = SwapTransactionParams {
1487 source_account: "INVALID_ACCOUNT".to_string(),
1488 source_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
1489 destination_asset: "native".to_string(),
1490 amount: 1_000_000,
1491 slippage_percent: 0.5,
1492 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1493 source_asset_decimals: None,
1494 destination_asset_decimals: None,
1495 };
1496
1497 let result = service.build_swap_transaction_xdr(¶ms, "e);
1498 assert!(result.is_err());
1499 }
1500
1501 #[test]
1502 fn test_build_swap_transaction_xdr_slippage_calculation() {
1503 let provider = create_mock_provider();
1504 let service = create_test_service(provider);
1505
1506 let quote = StellarQuoteResponse {
1508 input_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
1509 output_asset: "native".to_string(),
1510 in_amount: 1_000_000,
1511 out_amount: 1_000_000,
1512 price_impact_pct: 0.0,
1513 slippage_bps: 100, path: None,
1515 };
1516
1517 let params = SwapTransactionParams {
1518 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1519 source_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
1520 destination_asset: "native".to_string(),
1521 amount: 1_000_000,
1522 slippage_percent: 1.0,
1523 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1524 source_asset_decimals: Some(7),
1525 destination_asset_decimals: None,
1526 };
1527
1528 let xdr = service.build_swap_transaction_xdr(¶ms, "e).unwrap();
1529
1530 let envelope = TransactionEnvelope::from_xdr_base64(&xdr, Limits::none()).unwrap();
1532 match &envelope {
1533 TransactionEnvelope::Tx(env) => {
1534 match &env.tx.operations[0].body {
1535 OperationBody::InvokeHostFunction(op) => {
1536 match &op.host_function {
1537 HostFunction::InvokeContract(args) => {
1538 let amount_out_min =
1540 SoroswapService::<MockStellarProviderTrait>::scval_to_i128(
1541 &args.args[1],
1542 )
1543 .unwrap();
1544 assert_eq!(amount_out_min, 990_000);
1546 }
1547 _ => panic!("Expected InvokeContract"),
1548 }
1549 }
1550 _ => panic!("Expected InvokeHostFunction"),
1551 }
1552 }
1553 _ => panic!("Expected Tx envelope"),
1554 }
1555 }
1556
1557 #[tokio::test]
1560 async fn test_execute_swap_not_implemented() {
1561 let provider = create_mock_provider();
1562 let service = create_test_service(provider);
1563
1564 let params = SwapTransactionParams {
1565 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".to_string(),
1566 source_asset: "native".to_string(),
1567 destination_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
1568 .to_string(),
1569 amount: 1_000_000,
1570 slippage_percent: 0.5,
1571 network_passphrase: "Test SDF Network ; September 2015".to_string(),
1572 source_asset_decimals: None,
1573 destination_asset_decimals: Some(7),
1574 };
1575
1576 let result = service.execute_swap(params).await;
1577
1578 assert!(result.is_err());
1579 match result.unwrap_err() {
1580 StellarDexServiceError::UnknownError(msg) => {
1581 assert!(msg.contains("not yet implemented"));
1582 }
1583 e => panic!("Expected UnknownError, got {:?}", e),
1584 }
1585 }
1586
1587 #[tokio::test]
1590 async fn test_price_impact_calculation() {
1591 let mut mock = MockStellarProviderTrait::new();
1592
1593 let amounts: Vec<i128> = vec![1_000_000, 900_000];
1595 let sc_vals: Vec<ScVal> = amounts
1596 .iter()
1597 .map(|&a| SoroswapService::<MockStellarProviderTrait>::i128_to_scval(a))
1598 .collect();
1599 let sc_vec: ScVec = sc_vals.try_into().unwrap();
1600 let result_scval = ScVal::Vec(Some(sc_vec));
1601
1602 mock.expect_call_contract().returning(move |_, _, _| {
1603 let result = result_scval.clone();
1604 async move { Ok(result) }.boxed()
1605 });
1606
1607 let provider = Arc::new(mock);
1608 let service = create_test_service(provider);
1609
1610 let quote = service
1611 .get_token_to_xlm_quote(
1612 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
1613 1_000_000,
1614 0.5,
1615 None,
1616 )
1617 .await
1618 .unwrap();
1619
1620 assert_eq!(quote.price_impact_pct, 0.0);
1622 }
1623}