1use async_trait::async_trait;
7use chrono::Utc;
8use soroban_rs::xdr::{Limits, Operation, TransactionEnvelope, WriteXdr};
9use tracing::{debug, warn};
10
11use crate::constants::{
12 get_stellar_sponsored_transaction_validity_duration, STELLAR_DEFAULT_TRANSACTION_FEE,
13 STELLAR_LEDGER_TIME_SECONDS,
14};
15
16use crate::domain::relayer::{
17 stellar::utils::{apply_max_fee_slippage, get_expiration_ledger},
18 stellar::xdr_utils::{extract_source_account, parse_transaction_xdr},
19 GasAbstractionTrait, RelayerError, StellarRelayer,
20};
21use crate::domain::transaction::stellar::{
22 utils::{
23 add_operation_to_envelope, amount_to_ui_amount, convert_xlm_fee_to_token,
24 create_fee_payment_operation, estimate_fee, set_time_bounds, FeeQuote,
25 },
26 StellarTransactionValidator,
27};
28use crate::domain::xdr_needs_simulation;
29use crate::jobs::JobProducerTrait;
30use crate::models::{
31 transaction::stellar::OperationSpec, SponsoredTransactionBuildRequest,
32 SponsoredTransactionBuildResponse, SponsoredTransactionQuoteRequest,
33 SponsoredTransactionQuoteResponse, StellarFeeEstimateResult, StellarPrepareTransactionResult,
34 StellarTransactionData, TransactionInput,
35};
36use crate::models::{NetworkRepoModel, RelayerRepoModel, TransactionRepoModel};
37use crate::repositories::{
38 NetworkRepository, RelayerRepository, Repository, TransactionRepository,
39};
40use crate::services::provider::StellarProviderTrait;
41use crate::services::signer::StellarSignTrait;
42use crate::services::stellar_dex::StellarDexServiceTrait;
43use crate::services::stellar_fee_forwarder::{FeeForwarderParams, FeeForwarderService};
44use crate::services::TransactionCounterServiceTrait;
45use soroban_rs::xdr::{HostFunction, OperationBody, ReadXdr, ScVal};
46
47#[derive(Debug, Clone)]
49pub struct SorobanInvokeInfo {
50 pub target_contract: String,
52 pub target_fn: String,
54 pub target_args: Vec<ScVal>,
56}
57
58fn detect_soroban_invoke_from_xdr(xdr: &str) -> Result<Option<SorobanInvokeInfo>, RelayerError> {
66 use soroban_rs::xdr::TransactionEnvelope;
67
68 let envelope = TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
69 .map_err(|e| RelayerError::ValidationError(format!("Invalid XDR: {e}")))?;
70
71 let operations = match &envelope {
73 TransactionEnvelope::TxV0(env) => env.tx.operations.to_vec(),
74 TransactionEnvelope::Tx(env) => env.tx.operations.to_vec(),
75 TransactionEnvelope::TxFeeBump(env) => match &env.tx.inner_tx {
76 soroban_rs::xdr::FeeBumpTransactionInnerTx::Tx(inner) => inner.tx.operations.to_vec(),
77 },
78 };
79
80 let mut invoke_index = None;
81 let mut invoke_op = None;
82
83 for (idx, op) in operations.iter().enumerate() {
84 if let OperationBody::InvokeHostFunction(invoke) = &op.body {
85 invoke_index = Some(idx);
86 invoke_op = Some(invoke);
87 break;
88 }
89 }
90
91 if let Some(idx) = invoke_index {
92 if operations.len() != 1 {
94 return Err(RelayerError::ValidationError(
95 "Soroban transactions must contain exactly one operation".to_string(),
96 ));
97 }
98
99 let invoke_op = invoke_op.ok_or_else(|| {
101 RelayerError::ValidationError("InvokeHostFunction operation missing".to_string())
102 })?;
103
104 if idx != 0 {
105 return Err(RelayerError::ValidationError(
106 "InvokeHostFunction must be the first operation".to_string(),
107 ));
108 }
109
110 if let HostFunction::InvokeContract(invoke_args) = &invoke_op.host_function {
111 let target_contract = match &invoke_args.contract_address {
113 soroban_rs::xdr::ScAddress::Contract(contract_id) => {
114 stellar_strkey::Contract(contract_id.0 .0).to_string()
115 }
116 _ => {
117 return Err(RelayerError::ValidationError(
118 "InvokeHostFunction must target a contract address".to_string(),
119 ));
120 }
121 };
122
123 let target_fn = invoke_args.function_name.to_utf8_string_lossy();
125
126 let target_args: Vec<ScVal> = invoke_args.args.to_vec();
128
129 return Ok(Some(SorobanInvokeInfo {
130 target_contract,
131 target_fn,
132 target_args,
133 }));
134 }
135 }
136
137 Ok(None)
139}
140
141#[async_trait]
142impl<P, RR, NR, TR, J, TCS, S, D> GasAbstractionTrait
143 for StellarRelayer<P, RR, NR, TR, J, TCS, S, D>
144where
145 P: StellarProviderTrait + Send + Sync,
146 D: StellarDexServiceTrait + Send + Sync + 'static,
147 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
148 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
149 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
150 J: JobProducerTrait + Send + Sync + 'static,
151 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
152 S: StellarSignTrait + Send + Sync + 'static,
153{
154 async fn quote_sponsored_transaction(
155 &self,
156 params: SponsoredTransactionQuoteRequest,
157 ) -> Result<SponsoredTransactionQuoteResponse, RelayerError> {
158 let params = match params {
159 SponsoredTransactionQuoteRequest::Stellar(p) => p,
160 _ => {
161 return Err(RelayerError::ValidationError(
162 "Expected Stellar fee estimate request parameters".to_string(),
163 ));
164 }
165 };
166
167 if let Some(xdr) = ¶ms.transaction_xdr {
170 if let Some(soroban_info) = detect_soroban_invoke_from_xdr(xdr)? {
171 return self.quote_soroban_from_xdr(¶ms, &soroban_info).await;
172 }
173 }
174
175 self.quote_classic_sponsored(¶ms).await
177 }
178
179 async fn build_sponsored_transaction(
180 &self,
181 params: SponsoredTransactionBuildRequest,
182 ) -> Result<SponsoredTransactionBuildResponse, RelayerError> {
183 let params = match params {
184 SponsoredTransactionBuildRequest::Stellar(p) => p,
185 _ => {
186 return Err(RelayerError::ValidationError(
187 "Expected Stellar prepare transaction request parameters".to_string(),
188 ));
189 }
190 };
191
192 let policy = self.relayer.policies.get_stellar_policy();
193
194 StellarTransactionValidator::validate_allowed_token(¶ms.fee_token, &policy)
196 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
197
198 if !policy.is_user_fee_payment() {
200 return Err(RelayerError::ValidationError(
201 "Gas abstraction requires fee_payment_strategy: User".to_string(),
202 ));
203 }
204
205 if let Some(xdr) = ¶ms.transaction_xdr {
207 if let Some(soroban_info) = detect_soroban_invoke_from_xdr(xdr)? {
208 return self.build_soroban_sponsored(¶ms, &soroban_info).await;
209 }
210 }
211
212 self.build_classic_sponsored(¶ms).await
214 }
215}
216
217impl<P, RR, NR, TR, J, TCS, S, D> StellarRelayer<P, RR, NR, TR, J, TCS, S, D>
222where
223 P: StellarProviderTrait + Send + Sync,
224 D: StellarDexServiceTrait + Send + Sync + 'static,
225 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
226 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
227 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
228 J: JobProducerTrait + Send + Sync + 'static,
229 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
230 S: StellarSignTrait + Send + Sync + 'static,
231{
232 async fn quote_classic_sponsored(
237 &self,
238 params: &crate::models::StellarFeeEstimateRequestParams,
239 ) -> Result<SponsoredTransactionQuoteResponse, RelayerError> {
240 debug!(
241 "Processing classic quote sponsored transaction for token: {}",
242 params.fee_token
243 );
244
245 let policy = self.relayer.policies.get_stellar_policy();
246
247 StellarTransactionValidator::validate_allowed_token(¶ms.fee_token, &policy)
249 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
250
251 if params.transaction_xdr.is_none() && params.operations.is_none() {
253 return Err(RelayerError::ValidationError(
254 "Must provide either transaction_xdr or operations in the request".to_string(),
255 ));
256 }
257
258 let envelope = build_envelope_from_request(
260 params.transaction_xdr.as_ref(),
261 params.operations.as_ref(),
262 params.source_account.as_ref(),
263 &self.network.passphrase,
264 &self.provider,
265 )
266 .await?;
267
268 StellarTransactionValidator::gasless_transaction_validation(
270 &envelope,
271 &self.relayer.address,
272 &policy,
273 &self.provider,
274 None, )
276 .await
277 .map_err(|e| {
278 RelayerError::ValidationError(format!("Failed to validate gasless transaction: {e}"))
279 })?;
280
281 let inner_tx_fee = estimate_fee(&envelope, &self.provider, None)
283 .await
284 .map_err(crate::models::RelayerError::from)?;
285
286 let is_soroban = xdr_needs_simulation(&envelope).unwrap_or(false);
288 let additional_fees = if is_soroban {
289 0 } else {
291 2 * STELLAR_DEFAULT_TRANSACTION_FEE as u64 };
293 let xlm_fee = inner_tx_fee + additional_fees;
294
295 let fee_quote = convert_xlm_fee_to_token(
297 self.dex_service.as_ref(),
298 &policy,
299 xlm_fee,
300 ¶ms.fee_token,
301 )
302 .await
303 .map_err(crate::models::RelayerError::from)?;
304
305 StellarTransactionValidator::validate_max_fee(fee_quote.fee_in_stroops, &policy)
307 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
308
309 StellarTransactionValidator::validate_token_max_fee(
311 ¶ms.fee_token,
312 fee_quote.fee_in_token,
313 &policy,
314 )
315 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
316
317 StellarTransactionValidator::validate_user_token_balance(
319 &envelope,
320 ¶ms.fee_token,
321 fee_quote.fee_in_token,
322 &self.provider,
323 )
324 .await
325 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
326
327 debug!("Classic fee estimate result: {:?}", fee_quote);
328
329 Ok(SponsoredTransactionQuoteResponse::Stellar(
330 StellarFeeEstimateResult {
331 fee_in_token_ui: fee_quote.fee_in_token_ui,
332 fee_in_token: fee_quote.fee_in_token.to_string(),
333 conversion_rate: fee_quote.conversion_rate.to_string(),
334 max_fee_in_token: None,
336 max_fee_in_token_ui: None,
337 },
338 ))
339 }
340
341 async fn build_classic_sponsored(
346 &self,
347 params: &crate::models::StellarPrepareTransactionRequestParams,
348 ) -> Result<SponsoredTransactionBuildResponse, RelayerError> {
349 debug!(
350 "Processing classic build sponsored transaction for token: {}",
351 params.fee_token
352 );
353
354 let policy = self.relayer.policies.get_stellar_policy();
355
356 StellarTransactionValidator::validate_allowed_token(¶ms.fee_token, &policy)
358 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
359
360 if params.transaction_xdr.is_none() && params.operations.is_none() {
362 return Err(RelayerError::ValidationError(
363 "Must provide either transaction_xdr or operations in the request".to_string(),
364 ));
365 }
366
367 let envelope = build_envelope_from_request(
369 params.transaction_xdr.as_ref(),
370 params.operations.as_ref(),
371 params.source_account.as_ref(),
372 &self.network.passphrase,
373 &self.provider,
374 )
375 .await?;
376
377 StellarTransactionValidator::gasless_transaction_validation(
379 &envelope,
380 &self.relayer.address,
381 &policy,
382 &self.provider,
383 None, )
385 .await
386 .map_err(|e| {
387 RelayerError::ValidationError(format!("Failed to validate gasless transaction: {e}"))
388 })?;
389
390 let inner_tx_fee = estimate_fee(&envelope, &self.provider, None)
392 .await
393 .map_err(crate::models::RelayerError::from)?;
394
395 let is_soroban = xdr_needs_simulation(&envelope).unwrap_or(false);
397 let additional_fees = if is_soroban {
398 0
399 } else {
400 2 * STELLAR_DEFAULT_TRANSACTION_FEE as u64 };
402 let xlm_fee = inner_tx_fee + additional_fees;
403
404 debug!(
405 inner_tx_fee = inner_tx_fee,
406 additional_fees = additional_fees,
407 total_fee = xlm_fee,
408 "Fee estimated: inner transaction + fee payment op + fee-bump"
409 );
410
411 let fee_quote = convert_xlm_fee_to_token(
413 self.dex_service.as_ref(),
414 &policy,
415 xlm_fee,
416 ¶ms.fee_token,
417 )
418 .await
419 .map_err(crate::models::RelayerError::from)?;
420
421 StellarTransactionValidator::validate_max_fee(fee_quote.fee_in_stroops, &policy)
423 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
424
425 StellarTransactionValidator::validate_token_max_fee(
427 ¶ms.fee_token,
428 fee_quote.fee_in_token,
429 &policy,
430 )
431 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
432
433 StellarTransactionValidator::validate_user_token_balance(
435 &envelope,
436 ¶ms.fee_token,
437 fee_quote.fee_in_token,
438 &self.provider,
439 )
440 .await
441 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
442
443 let mut final_envelope = add_payment_operation_to_envelope(
445 envelope,
446 &fee_quote,
447 ¶ms.fee_token,
448 &self.relayer.address,
449 )?;
450
451 debug!(
452 estimated_fee = xlm_fee,
453 final_fee_in_token = fee_quote.fee_in_token_ui,
454 "Classic transaction prepared successfully"
455 );
456
457 let valid_until = Utc::now() + get_stellar_sponsored_transaction_validity_duration();
459 set_time_bounds(&mut final_envelope, valid_until)
460 .map_err(crate::models::RelayerError::from)?;
461
462 let extended_xdr = final_envelope
464 .to_xdr_base64(Limits::none())
465 .map_err(|e| RelayerError::Internal(format!("Failed to serialize XDR: {e}")))?;
466
467 Ok(SponsoredTransactionBuildResponse::Stellar(
468 StellarPrepareTransactionResult {
469 transaction: extended_xdr,
470 fee_in_token: fee_quote.fee_in_token.to_string(),
471 fee_in_token_ui: fee_quote.fee_in_token_ui,
472 fee_in_stroops: fee_quote.fee_in_stroops.to_string(),
473 fee_token: params.fee_token.clone(),
474 valid_until: valid_until.to_rfc3339(),
475 user_auth_entry: None,
477 max_fee_in_token: None,
479 max_fee_in_token_ui: None,
480 },
481 ))
482 }
483}
484
485impl<P, RR, NR, TR, J, TCS, S, D> StellarRelayer<P, RR, NR, TR, J, TCS, S, D>
490where
491 P: StellarProviderTrait + Send + Sync,
492 D: StellarDexServiceTrait + Send + Sync + 'static,
493 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
494 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
495 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
496 J: JobProducerTrait + Send + Sync + 'static,
497 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
498 S: StellarSignTrait + Send + Sync + 'static,
499{
500 async fn quote_soroban_from_xdr(
505 &self,
506 params: &crate::models::StellarFeeEstimateRequestParams,
507 soroban_info: &SorobanInvokeInfo,
508 ) -> Result<SponsoredTransactionQuoteResponse, RelayerError> {
509 debug!(
510 "Processing Soroban quote request for token: {}, target: {}::{}",
511 params.fee_token, soroban_info.target_contract, soroban_info.target_fn
512 );
513
514 let policy = self.relayer.policies.get_stellar_policy();
515
516 if !policy.is_user_fee_payment() {
518 return Err(RelayerError::ValidationError(
519 "Gas abstraction requires fee_payment_strategy: User".to_string(),
520 ));
521 }
522
523 StellarTransactionValidator::validate_allowed_token(¶ms.fee_token, &policy)
525 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
526
527 let fee_forwarder = crate::config::ServerConfig::resolve_stellar_fee_forwarder_address(
529 self.network.is_testnet(),
530 )
531 .ok_or_else(|| {
532 let env_var = if self.network.is_testnet() {
533 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS"
534 } else {
535 "STELLAR_MAINNET_FEE_FORWARDER_ADDRESS"
536 };
537 RelayerError::ValidationError(format!(
538 "FeeForwarder address not configured. Set {env_var} env var."
539 ))
540 })?;
541
542 if stellar_strkey::Contract::from_string(¶ms.fee_token).is_err() {
544 return Err(RelayerError::ValidationError(format!(
545 "fee_token must be a valid Soroban contract address (C...), got '{}'",
546 params.fee_token
547 )));
548 }
549
550 let xdr = params.transaction_xdr.as_ref().ok_or_else(|| {
555 RelayerError::ValidationError(
556 "Soroban gas abstraction requires transaction_xdr".to_string(),
557 )
558 })?;
559
560 let source_envelope = TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
561 .map_err(|e| RelayerError::ValidationError(format!("Invalid XDR: {e}")))?;
562 let user_address = extract_source_account(&source_envelope).map_err(|e| {
563 RelayerError::ValidationError(format!("Failed to extract source account: {e}"))
564 })?;
565
566 let base_fee_stroops: u64 = STELLAR_DEFAULT_TRANSACTION_FEE as u64;
568 let base_fee_quote = convert_xlm_fee_to_token(
569 self.dex_service.as_ref(),
570 &policy,
571 base_fee_stroops,
572 ¶ms.fee_token,
573 )
574 .await
575 .map_err(crate::models::RelayerError::from)?;
576
577 let validity_duration = get_stellar_sponsored_transaction_validity_duration();
578 let validity_seconds = validity_duration.num_seconds() as u64;
579 let expiration_ledger = get_expiration_ledger(&self.provider, validity_seconds)
580 .await
581 .map_err(|e| RelayerError::Internal(format!("Failed to get expiration ledger: {e}")))?;
582
583 let fee_params = FeeForwarderParams {
584 fee_token: params.fee_token.clone(),
585 fee_amount: base_fee_quote.fee_in_token as i128,
586 max_fee_amount: apply_max_fee_slippage(base_fee_quote.fee_in_token),
587 expiration_ledger,
588 target_contract: soroban_info.target_contract.clone(),
589 target_fn: soroban_info.target_fn.clone(),
590 target_args: soroban_info.target_args.clone(),
591 user: user_address,
592 relayer: self.relayer.address.clone(),
593 };
594
595 let invoke_op = FeeForwarderService::<P>::build_invoke_operation_standalone(
601 &fee_forwarder,
602 &fee_params,
603 vec![],
604 )
605 .map_err(|e| RelayerError::Internal(format!("Failed to build invoke operation: {e}")))?;
606
607 let envelope = build_soroban_transaction_envelope(
608 &self.relayer.address,
609 invoke_op,
610 base_fee_stroops as u32,
611 )?;
612
613 let sim_response = self
614 .provider
615 .simulate_transaction_envelope(&envelope)
616 .await
617 .map_err(|e| RelayerError::Internal(format!("Failed to simulate transaction: {e}")))?;
618
619 let total_fee = calculate_total_soroban_fee(&sim_response, 1)?;
620
621 let fee_quote = convert_xlm_fee_to_token(
622 self.dex_service.as_ref(),
623 &policy,
624 total_fee as u64,
625 ¶ms.fee_token,
626 )
627 .await
628 .map_err(crate::models::RelayerError::from)?;
629
630 debug!(
631 "Soroban fee estimate: {} stroops, {} token",
632 fee_quote.fee_in_stroops, fee_quote.fee_in_token
633 );
634
635 StellarTransactionValidator::validate_max_fee(fee_quote.fee_in_stroops, &policy)
637 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
638
639 StellarTransactionValidator::validate_token_max_fee(
641 ¶ms.fee_token,
642 fee_quote.fee_in_token,
643 &policy,
644 )
645 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
646
647 StellarTransactionValidator::validate_user_token_balance(
649 &source_envelope,
650 ¶ms.fee_token,
651 fee_quote.fee_in_token,
652 &self.provider,
653 )
654 .await
655 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
656
657 let max_fee_in_token = apply_max_fee_slippage(fee_quote.fee_in_token);
659 let token_decimals = policy
660 .get_allowed_token_decimals(¶ms.fee_token)
661 .unwrap_or(7);
662 let max_fee_in_token_ui = amount_to_ui_amount(max_fee_in_token as u64, token_decimals);
663
664 let result = StellarFeeEstimateResult {
666 fee_in_token_ui: fee_quote.fee_in_token_ui,
667 fee_in_token: fee_quote.fee_in_token.to_string(),
668 conversion_rate: fee_quote.conversion_rate.to_string(),
669 max_fee_in_token: Some(max_fee_in_token.to_string()),
670 max_fee_in_token_ui: Some(max_fee_in_token_ui),
671 };
672
673 Ok(SponsoredTransactionQuoteResponse::Stellar(result))
674 }
675
676 async fn build_soroban_sponsored(
681 &self,
682 params: &crate::models::StellarPrepareTransactionRequestParams,
683 soroban_info: &SorobanInvokeInfo,
684 ) -> Result<SponsoredTransactionBuildResponse, RelayerError> {
685 debug!(
686 "Processing Soroban build request for token: {}, target: {}::{}",
687 params.fee_token, soroban_info.target_contract, soroban_info.target_fn
688 );
689
690 let policy = self.relayer.policies.get_stellar_policy();
691
692 let fee_forwarder = crate::config::ServerConfig::resolve_stellar_fee_forwarder_address(
696 self.network.is_testnet(),
697 )
698 .ok_or_else(|| {
699 let env_var = if self.network.is_testnet() {
700 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS"
701 } else {
702 "STELLAR_MAINNET_FEE_FORWARDER_ADDRESS"
703 };
704 RelayerError::ValidationError(format!(
705 "FeeForwarder address not configured. Set {env_var} env var."
706 ))
707 })?;
708
709 if stellar_strkey::Contract::from_string(¶ms.fee_token).is_err() {
711 return Err(RelayerError::ValidationError(format!(
712 "fee_token must be a valid Soroban contract address (C...), got '{}'",
713 params.fee_token
714 )));
715 }
716
717 let xdr = params.transaction_xdr.as_ref().ok_or_else(|| {
720 RelayerError::ValidationError(
721 "Soroban gas abstraction requires transaction_xdr".to_string(),
722 )
723 })?;
724
725 let source_envelope = TransactionEnvelope::from_xdr_base64(xdr, Limits::none())
726 .map_err(|e| RelayerError::ValidationError(format!("Invalid XDR: {e}")))?;
727 let user_address = extract_source_account(&source_envelope).map_err(|e| {
728 RelayerError::ValidationError(format!("Failed to extract source account: {e}"))
729 })?;
730
731 let validity_duration = get_stellar_sponsored_transaction_validity_duration();
733 let validity_seconds = validity_duration.num_seconds() as u64;
734 let expiration_ledger = get_expiration_ledger(&self.provider, validity_seconds)
735 .await
736 .map_err(|e| RelayerError::Internal(format!("Failed to get expiration ledger: {e}")))?;
737
738 let base_fee_stroops: u64 = STELLAR_DEFAULT_TRANSACTION_FEE as u64;
740 let base_fee_quote = convert_xlm_fee_to_token(
741 self.dex_service.as_ref(),
742 &policy,
743 base_fee_stroops,
744 ¶ms.fee_token,
745 )
746 .await
747 .map_err(crate::models::RelayerError::from)?;
748
749 let mut fee_params = FeeForwarderParams {
751 fee_token: params.fee_token.clone(),
752 fee_amount: base_fee_quote.fee_in_token as i128,
753 max_fee_amount: apply_max_fee_slippage(base_fee_quote.fee_in_token),
754 expiration_ledger,
755 target_contract: soroban_info.target_contract.clone(),
756 target_fn: soroban_info.target_fn.clone(),
757 target_args: soroban_info.target_args.clone(),
758 user: user_address.clone(),
759 relayer: self.relayer.address.clone(),
760 };
761
762 let invoke_op = FeeForwarderService::<P>::build_invoke_operation_standalone(
767 &fee_forwarder,
768 &fee_params,
769 vec![], )
771 .map_err(|e| RelayerError::Internal(format!("Failed to build invoke operation: {e}")))?;
772
773 let envelope = build_soroban_transaction_envelope(
774 &self.relayer.address,
775 invoke_op,
776 base_fee_stroops as u32,
777 )?;
778
779 let sim_response = self
780 .provider
781 .simulate_transaction_envelope(&envelope)
782 .await
783 .map_err(|e| RelayerError::Internal(format!("Failed to simulate transaction: {e}")))?;
784
785 let total_fee = calculate_total_soroban_fee(&sim_response, 1)?;
786
787 let fee_quote = convert_xlm_fee_to_token(
788 self.dex_service.as_ref(),
789 &policy,
790 total_fee as u64,
791 ¶ms.fee_token,
792 )
793 .await
794 .map_err(crate::models::RelayerError::from)?;
795
796 StellarTransactionValidator::validate_max_fee(fee_quote.fee_in_stroops, &policy)
798 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
799
800 StellarTransactionValidator::validate_token_max_fee(
802 ¶ms.fee_token,
803 fee_quote.fee_in_token,
804 &policy,
805 )
806 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
807
808 StellarTransactionValidator::validate_user_token_balance(
810 &source_envelope,
811 ¶ms.fee_token,
812 fee_quote.fee_in_token,
813 &self.provider,
814 )
815 .await
816 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
817
818 fee_params.fee_amount = fee_quote.fee_in_token as i128;
821 fee_params.max_fee_amount = apply_max_fee_slippage(fee_quote.fee_in_token);
822
823 let user_auth_entry = FeeForwarderService::<P>::build_user_auth_entry_standalone(
825 &fee_forwarder,
826 &fee_params,
827 true,
828 )
829 .map_err(|e| RelayerError::Internal(format!("Failed to build user auth entry: {e}")))?;
830
831 let user_auth_xdr = FeeForwarderService::<P>::serialize_auth_entry(&user_auth_entry)
832 .map_err(|e| RelayerError::Internal(format!("Failed to serialize auth entry: {e}")))?;
833
834 let relayer_auth_entry = FeeForwarderService::<P>::build_relayer_auth_entry_standalone(
836 &fee_forwarder,
837 &fee_params,
838 )
839 .map_err(|e| RelayerError::Internal(format!("Failed to build relayer auth entry: {e}")))?;
840
841 let invoke_op = FeeForwarderService::<P>::build_invoke_operation_standalone(
846 &fee_forwarder,
847 &fee_params,
848 vec![user_auth_entry, relayer_auth_entry],
849 )
850 .map_err(|e| RelayerError::Internal(format!("Failed to build invoke operation: {e}")))?;
851
852 let mut envelope = build_soroban_transaction_envelope(
853 &self.relayer.address,
854 invoke_op,
855 base_fee_stroops as u32,
856 )?;
857
858 apply_simulation_to_soroban_envelope(&mut envelope, &sim_response, 1)?;
862
863 let transaction_xdr = envelope
864 .to_xdr_base64(Limits::none())
865 .map_err(|e| RelayerError::Internal(format!("Failed to serialize transaction: {e}")))?;
866
867 let current_ledger =
870 self.provider.get_latest_ledger().await.map_err(|e| {
871 RelayerError::Internal(format!("Failed to get current ledger: {e}"))
872 })?;
873 let ledgers_until_expiration = expiration_ledger.saturating_sub(current_ledger.sequence);
874 let seconds_until_expiration =
875 ledgers_until_expiration as u64 * STELLAR_LEDGER_TIME_SECONDS;
876 let valid_until = Utc::now() + chrono::Duration::seconds(seconds_until_expiration as i64);
877
878 debug!(
879 "Soroban build complete: transaction_xdr length={}, auth_xdr length={}, expiration_ledger={}, valid_until={}",
880 transaction_xdr.len(),
881 user_auth_xdr.len(),
882 expiration_ledger,
883 valid_until.to_rfc3339()
884 );
885
886 let max_fee_in_token = fee_params.max_fee_amount;
888 let token_decimals = policy
889 .get_allowed_token_decimals(¶ms.fee_token)
890 .unwrap_or(7);
891 let max_fee_in_token_ui = amount_to_ui_amount(max_fee_in_token as u64, token_decimals);
892
893 let result = StellarPrepareTransactionResult {
895 transaction: transaction_xdr,
896 fee_in_token: fee_quote.fee_in_token.to_string(),
897 fee_in_token_ui: fee_quote.fee_in_token_ui,
898 fee_in_stroops: fee_quote.fee_in_stroops.to_string(),
899 fee_token: params.fee_token.clone(),
900 valid_until: valid_until.to_rfc3339(),
901 user_auth_entry: Some(user_auth_xdr),
903 max_fee_in_token: Some(max_fee_in_token.to_string()),
904 max_fee_in_token_ui: Some(max_fee_in_token_ui),
905 };
906
907 Ok(SponsoredTransactionBuildResponse::Stellar(result))
908 }
909}
910
911fn build_soroban_transaction_envelope(
913 source_address: &str,
914 operation: Operation,
915 fee: u32,
916) -> Result<TransactionEnvelope, RelayerError> {
917 use soroban_rs::xdr::{
918 Memo, MuxedAccount, Preconditions, SequenceNumber, Transaction, TransactionExt,
919 TransactionV1Envelope, Uint256, VecM,
920 };
921
922 let pk = stellar_strkey::ed25519::PublicKey::from_string(source_address)
924 .map_err(|e| RelayerError::ValidationError(format!("Invalid source address: {e}")))?;
925 let source = MuxedAccount::Ed25519(Uint256(pk.0));
926
927 let tx = Transaction {
929 source_account: source,
930 fee,
931 seq_num: SequenceNumber(0),
932 cond: Preconditions::None,
933 memo: Memo::None,
934 operations: vec![operation].try_into().map_err(|_| {
935 RelayerError::Internal("Failed to create operations vector".to_string())
936 })?,
937 ext: TransactionExt::V0,
938 };
939
940 Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
941 tx,
942 signatures: VecM::default(),
943 }))
944}
945
946fn calculate_total_soroban_fee(
948 sim_response: &soroban_rs::stellar_rpc_client::SimulateTransactionResponse,
949 operations_count: u64,
950) -> Result<u32, RelayerError> {
951 if let Some(err) = sim_response.error.clone() {
952 return Err(RelayerError::ValidationError(format!(
953 "Simulation failed: {err}"
954 )));
955 }
956
957 let inclusion_fee = operations_count * STELLAR_DEFAULT_TRANSACTION_FEE as u64;
958 let resource_fee = sim_response.min_resource_fee;
959 let total_fee = inclusion_fee + resource_fee;
960 let total_fee_u32 = u32::try_from(total_fee)
961 .map_err(|_| RelayerError::Internal("Soroban fee exceeds u32::MAX".to_string()))?;
962
963 Ok(total_fee_u32.max(STELLAR_DEFAULT_TRANSACTION_FEE))
964}
965
966fn apply_simulation_to_soroban_envelope(
968 envelope: &mut TransactionEnvelope,
969 sim_response: &soroban_rs::stellar_rpc_client::SimulateTransactionResponse,
970 operations_count: u64,
971) -> Result<(), RelayerError> {
972 use soroban_rs::xdr::SorobanTransactionData;
973
974 let total_fee = calculate_total_soroban_fee(sim_response, operations_count)?;
975
976 let tx_data = SorobanTransactionData::from_xdr_base64(
977 sim_response.transaction_data.as_str(),
978 Limits::none(),
979 )
980 .map_err(|e| RelayerError::Internal(format!("Invalid transaction_data XDR: {e}")))?;
981
982 match envelope {
983 TransactionEnvelope::Tx(ref mut env) => {
984 env.tx.fee = total_fee;
985 env.tx.ext = soroban_rs::xdr::TransactionExt::V1(tx_data);
986 }
987 TransactionEnvelope::TxV0(_) | TransactionEnvelope::TxFeeBump(_) => {
988 return Err(RelayerError::Internal(
989 "Soroban transaction must be a V1 envelope".to_string(),
990 ));
991 }
992 }
993
994 Ok(())
995}
996
997fn add_payment_operation_to_envelope(
1015 mut envelope: TransactionEnvelope,
1016 fee_quote: &FeeQuote,
1017 fee_token: &str,
1018 relayer_address: &str,
1019) -> Result<TransactionEnvelope, RelayerError> {
1020 let fee_amount = i64::try_from(fee_quote.fee_in_token).map_err(|_| {
1022 RelayerError::Internal(
1023 "Fee amount too large for payment operation (exceeds i64::MAX)".to_string(),
1024 )
1025 })?;
1026
1027 let is_soroban = xdr_needs_simulation(&envelope).unwrap_or(false);
1028 if !is_soroban {
1030 add_fee_payment_operation(&mut envelope, fee_token, fee_amount, relayer_address)?;
1032 }
1033
1034 Ok(envelope)
1035}
1036
1037async fn build_envelope_from_request<P>(
1047 transaction_xdr: Option<&String>,
1048 operations: Option<&Vec<OperationSpec>>,
1049 source_account: Option<&String>,
1050 network_passphrase: &str,
1051 provider: &P,
1052) -> Result<TransactionEnvelope, RelayerError>
1053where
1054 P: StellarProviderTrait + Send + Sync,
1055{
1056 if let Some(xdr) = transaction_xdr {
1057 parse_transaction_xdr(xdr, false)
1058 .map_err(|e| RelayerError::Internal(format!("Failed to parse XDR: {e}")))
1059 } else if let Some(ops) = operations {
1060 let source_account = source_account.ok_or_else(|| {
1062 RelayerError::ValidationError(
1063 "source_account is required when providing operations".to_string(),
1064 )
1065 })?;
1066
1067 let account_entry = provider.get_account(source_account).await.map_err(|e| {
1071 warn!(
1072 source_account = %source_account,
1073 error = %e,
1074 "get_account failed in build_envelope_from_request (called before transaction creation)"
1075 );
1076 RelayerError::Internal(format!(
1079 "Failed to fetch account sequence number for {source_account}: {e}",
1080 ))
1081 })?;
1082
1083 let next_sequence = account_entry.seq_num.0 + 1;
1085
1086 let stellar_data = StellarTransactionData {
1087 source_account: source_account.clone(),
1088 fee: None,
1089 sequence_number: Some(next_sequence as i64),
1090 memo: None,
1091 valid_until: None,
1092 network_passphrase: network_passphrase.to_string(),
1093 signatures: vec![],
1094 hash: None,
1095 simulation_transaction_data: None,
1096 transaction_input: TransactionInput::Operations(ops.clone()),
1097 signed_envelope_xdr: None,
1098 transaction_result_xdr: None,
1099 };
1100
1101 stellar_data.build_unsigned_envelope().map_err(|e| {
1103 RelayerError::Internal(format!("Failed to build envelope from operations: {e}"))
1104 })
1105 } else {
1106 Err(RelayerError::ValidationError(
1107 "Must provide either transaction_xdr or operations in the request".to_string(),
1108 ))
1109 }
1110}
1111
1112fn add_fee_payment_operation(
1114 envelope: &mut TransactionEnvelope,
1115 fee_token: &str,
1116 fee_amount: i64,
1117 relayer_address: &str,
1118) -> Result<(), RelayerError> {
1119 let payment_op_spec = create_fee_payment_operation(relayer_address, fee_token, fee_amount)
1120 .map_err(crate::models::RelayerError::from)?;
1121
1122 let payment_op = Operation::try_from(payment_op_spec)
1124 .map_err(|e| RelayerError::Internal(format!("Failed to convert payment operation: {e}")))?;
1125
1126 add_operation_to_envelope(envelope, payment_op).map_err(crate::models::RelayerError::from)?;
1128
1129 Ok(())
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135 use crate::domain::transaction::stellar::utils::parse_account_id;
1136 use crate::services::stellar_dex::AssetType;
1137 use crate::{
1138 config::{NetworkConfigCommon, StellarNetworkConfig},
1139 jobs::MockJobProducerTrait,
1140 models::{
1141 transaction::stellar::OperationSpec, AssetSpec, NetworkConfigData, NetworkRepoModel,
1142 NetworkType, RelayerNetworkPolicy, RelayerRepoModel, RelayerStellarPolicy, RpcConfig,
1143 SponsoredTransactionBuildRequest, SponsoredTransactionQuoteRequest,
1144 },
1145 repositories::{
1146 InMemoryNetworkRepository, MockRelayerRepository, MockTransactionRepository,
1147 },
1148 services::{
1149 provider::MockStellarProviderTrait, signer::MockStellarSignTrait,
1150 stellar_dex::MockStellarDexServiceTrait, MockTransactionCounterServiceTrait,
1151 },
1152 };
1153 use mockall::predicate::*;
1154 use serial_test::serial;
1155 use soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse;
1156 use soroban_rs::stellar_rpc_client::LedgerEntryResult;
1157 use soroban_rs::xdr::{
1158 AccountEntry, AccountEntryExt, AccountId, AlphaNum4, AssetCode4, LedgerEntry,
1159 LedgerEntryData, LedgerEntryExt, LedgerKey, Limits, MuxedAccount, Operation, OperationBody,
1160 PaymentOp, Preconditions, PublicKey, SequenceNumber, String32, Thresholds, Transaction,
1161 TransactionEnvelope, TransactionExt, TransactionV1Envelope, TrustLineEntry,
1162 TrustLineEntryExt, Uint256, VecM, WriteXdr,
1163 };
1164 use std::future::ready;
1165 use std::sync::Arc;
1166 use stellar_strkey::ed25519::PublicKey as Ed25519PublicKey;
1167
1168 const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
1169 const TEST_NETWORK_PASSPHRASE: &str = "Test SDF Network ; September 2015";
1170 const USDC_ASSET: &str = "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
1171
1172 fn create_test_transaction_xdr() -> String {
1174 let source_pk = Ed25519PublicKey::from_string(
1176 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
1177 )
1178 .unwrap();
1179 let dest_pk = Ed25519PublicKey::from_string(
1180 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
1181 )
1182 .unwrap();
1183
1184 let payment_op = PaymentOp {
1185 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
1186 asset: soroban_rs::xdr::Asset::Native,
1187 amount: 1000000,
1188 };
1189
1190 let operation = Operation {
1191 source_account: None,
1192 body: OperationBody::Payment(payment_op),
1193 };
1194
1195 let operations: VecM<Operation, 100> = vec![operation].try_into().unwrap();
1196
1197 let tx = Transaction {
1198 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
1199 fee: 100,
1200 seq_num: SequenceNumber(2), cond: Preconditions::None,
1202 memo: soroban_rs::xdr::Memo::None,
1203 operations,
1204 ext: TransactionExt::V0,
1205 };
1206
1207 let envelope = TransactionV1Envelope {
1208 tx,
1209 signatures: vec![].try_into().unwrap(),
1210 };
1211
1212 let tx_envelope = TransactionEnvelope::Tx(envelope);
1213 tx_envelope.to_xdr_base64(Limits::none()).unwrap()
1214 }
1215
1216 fn create_test_relayer_with_user_fee_strategy() -> RelayerRepoModel {
1218 let mut policy = RelayerStellarPolicy::default();
1219 policy.fee_payment_strategy = Some(crate::models::StellarFeePaymentStrategy::User);
1220 policy.allowed_tokens = Some(vec![crate::models::StellarAllowedTokensPolicy {
1221 asset: USDC_ASSET.to_string(),
1222 metadata: None,
1223 max_allowed_fee: None,
1224 swap_config: None,
1225 }]);
1226
1227 RelayerRepoModel {
1228 id: "test-relayer-id".to_string(),
1229 name: "Test Relayer".to_string(),
1230 network: "testnet".to_string(),
1231 paused: false,
1232 network_type: NetworkType::Stellar,
1233 signer_id: "signer-id".to_string(),
1234 policies: RelayerNetworkPolicy::Stellar(policy),
1235 address: TEST_PK.to_string(),
1236 notification_id: Some("notification-id".to_string()),
1237 system_disabled: false,
1238 custom_rpc_urls: None,
1239 ..Default::default()
1240 }
1241 }
1242
1243 fn create_mock_dex_service() -> Arc<MockStellarDexServiceTrait> {
1245 let mut mock_dex = MockStellarDexServiceTrait::new();
1246 mock_dex
1247 .expect_supported_asset_types()
1248 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1249 Arc::new(mock_dex)
1250 }
1251
1252 fn create_test_network() -> NetworkRepoModel {
1254 NetworkRepoModel {
1255 id: "stellar:testnet".to_string(),
1256 name: "testnet".to_string(),
1257 network_type: NetworkType::Stellar,
1258 config: NetworkConfigData::Stellar(StellarNetworkConfig {
1259 common: NetworkConfigCommon {
1260 network: "testnet".to_string(),
1261 from: None,
1262 rpc_urls: Some(vec![RpcConfig::new(
1263 "https://horizon-testnet.stellar.org".to_string(),
1264 )]),
1265 explorer_urls: None,
1266 average_blocktime_ms: Some(5000),
1267 is_testnet: Some(true),
1268 tags: None,
1269 },
1270 passphrase: Some(TEST_NETWORK_PASSPHRASE.to_string()),
1271 horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
1272 }),
1273 }
1274 }
1275
1276 fn create_test_mainnet_network() -> NetworkRepoModel {
1278 NetworkRepoModel {
1279 id: "stellar:mainnet".to_string(),
1280 name: "mainnet".to_string(),
1281 network_type: NetworkType::Stellar,
1282 config: NetworkConfigData::Stellar(StellarNetworkConfig {
1283 common: NetworkConfigCommon {
1284 network: "mainnet".to_string(),
1285 from: None,
1286 rpc_urls: Some(vec![RpcConfig::new(
1287 "https://horizon.stellar.org".to_string(),
1288 )]),
1289 explorer_urls: None,
1290 average_blocktime_ms: Some(5000),
1291 is_testnet: Some(false),
1292 tags: None,
1293 },
1294 passphrase: Some("Public Global Stellar Network ; September 2015".to_string()),
1295 horizon_url: Some("https://horizon.stellar.org".to_string()),
1296 }),
1297 }
1298 }
1299
1300 async fn create_test_relayer_instance(
1302 relayer_model: RelayerRepoModel,
1303 provider: MockStellarProviderTrait,
1304 dex_service: Arc<MockStellarDexServiceTrait>,
1305 ) -> crate::domain::relayer::stellar::StellarRelayer<
1306 MockStellarProviderTrait,
1307 MockRelayerRepository,
1308 InMemoryNetworkRepository,
1309 MockTransactionRepository,
1310 MockJobProducerTrait,
1311 MockTransactionCounterServiceTrait,
1312 MockStellarSignTrait,
1313 MockStellarDexServiceTrait,
1314 > {
1315 let network_repository = Arc::new(InMemoryNetworkRepository::new());
1316 let test_network = create_test_network();
1317 network_repository.create(test_network).await.unwrap();
1318
1319 let relayer_repo = Arc::new(MockRelayerRepository::new());
1320 let tx_repo = Arc::new(MockTransactionRepository::new());
1321 let job_producer = Arc::new(MockJobProducerTrait::new());
1322 let counter = Arc::new(MockTransactionCounterServiceTrait::new());
1323 let signer = Arc::new(MockStellarSignTrait::new());
1324
1325 crate::domain::relayer::stellar::StellarRelayer::new(
1326 relayer_model,
1327 signer,
1328 provider,
1329 crate::domain::relayer::stellar::StellarRelayerDependencies::new(
1330 relayer_repo,
1331 network_repository,
1332 tx_repo,
1333 counter,
1334 job_producer,
1335 ),
1336 dex_service,
1337 )
1338 .await
1339 .unwrap()
1340 }
1341
1342 async fn create_test_relayer_instance_with_network(
1344 relayer_model: RelayerRepoModel,
1345 provider: MockStellarProviderTrait,
1346 dex_service: Arc<MockStellarDexServiceTrait>,
1347 network: NetworkRepoModel,
1348 ) -> crate::domain::relayer::stellar::StellarRelayer<
1349 MockStellarProviderTrait,
1350 MockRelayerRepository,
1351 InMemoryNetworkRepository,
1352 MockTransactionRepository,
1353 MockJobProducerTrait,
1354 MockTransactionCounterServiceTrait,
1355 MockStellarSignTrait,
1356 MockStellarDexServiceTrait,
1357 > {
1358 let network_repository = Arc::new(InMemoryNetworkRepository::new());
1359 network_repository.create(network).await.unwrap();
1360
1361 let relayer_repo = Arc::new(MockRelayerRepository::new());
1362 let tx_repo = Arc::new(MockTransactionRepository::new());
1363 let job_producer = Arc::new(MockJobProducerTrait::new());
1364 let counter = Arc::new(MockTransactionCounterServiceTrait::new());
1365 let signer = Arc::new(MockStellarSignTrait::new());
1366
1367 crate::domain::relayer::stellar::StellarRelayer::new(
1368 relayer_model,
1369 signer,
1370 provider,
1371 crate::domain::relayer::stellar::StellarRelayerDependencies::new(
1372 relayer_repo,
1373 network_repository,
1374 tx_repo,
1375 counter,
1376 job_producer,
1377 ),
1378 dex_service,
1379 )
1380 .await
1381 .unwrap()
1382 }
1383
1384 #[tokio::test]
1385 async fn test_quote_sponsored_transaction_with_xdr() {
1386 let relayer_model = create_test_relayer_with_user_fee_strategy();
1387 let mut provider = MockStellarProviderTrait::new();
1388
1389 provider.expect_get_account().returning(|_| {
1391 Box::pin(ready(Ok(AccountEntry {
1392 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1393 balance: 1000000000,
1394 seq_num: SequenceNumber(1),
1395 num_sub_entries: 0,
1396 inflation_dest: None,
1397 flags: 0,
1398 home_domain: String32::default(),
1399 thresholds: Thresholds([0; 4]),
1400 signers: VecM::default(),
1401 ext: AccountEntryExt::V0,
1402 })))
1403 });
1404
1405 provider.expect_get_ledger_entries().returning(|keys| {
1408 let account_id = if let Some(LedgerKey::Trustline(trustline_key)) = keys.first() {
1410 trustline_key.account_id.clone()
1411 } else {
1412 parse_account_id(TEST_PK)
1414 .unwrap_or(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))))
1415 };
1416
1417 let issuer_id =
1418 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
1419 .unwrap_or(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))));
1420
1421 let trustline_entry = TrustLineEntry {
1423 account_id,
1424 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1425 asset_code: AssetCode4(*b"USDC"),
1426 issuer: issuer_id,
1427 }),
1428 balance: 10_000_000i64,
1429 limit: i64::MAX,
1430 flags: 0,
1431 ext: TrustLineEntryExt::V0,
1432 };
1433
1434 let ledger_entry = LedgerEntry {
1435 last_modified_ledger_seq: 0,
1436 data: LedgerEntryData::Trustline(trustline_entry),
1437 ext: LedgerEntryExt::V0,
1438 };
1439
1440 let xdr = ledger_entry
1442 .data
1443 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1444 .expect("Failed to encode trustline entry data to XDR");
1445
1446 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1447 entries: Some(vec![LedgerEntryResult {
1448 key: "test_key".to_string(),
1449 xdr,
1450 last_modified_ledger: 0u32,
1451 live_until_ledger_seq_ledger_seq: None,
1452 }]),
1453 latest_ledger: 0,
1454 })))
1455 });
1456
1457 let mut dex_service = MockStellarDexServiceTrait::new();
1458 dex_service
1459 .expect_supported_asset_types()
1460 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1461
1462 dex_service
1464 .expect_get_xlm_to_token_quote()
1465 .returning(|_, _, _, _| {
1466 Box::pin(ready(Ok(
1467 crate::services::stellar_dex::StellarQuoteResponse {
1468 input_asset: "native".to_string(),
1469 output_asset: USDC_ASSET.to_string(),
1470 in_amount: 100000,
1471 out_amount: 1500000,
1472 price_impact_pct: 0.0,
1473 slippage_bps: 100,
1474 path: None,
1475 },
1476 )))
1477 });
1478
1479 let dex_service = Arc::new(dex_service);
1480 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1481
1482 let transaction_xdr = create_test_transaction_xdr();
1483 let request = SponsoredTransactionQuoteRequest::Stellar(
1484 crate::models::StellarFeeEstimateRequestParams {
1485 transaction_xdr: Some(transaction_xdr),
1486 operations: None,
1487 source_account: None,
1488 fee_token: USDC_ASSET.to_string(),
1489 },
1490 );
1491
1492 let result = relayer.quote_sponsored_transaction(request).await;
1493 if let Err(e) = &result {
1494 eprintln!("Quote error: {e:?}");
1495 }
1496 assert!(result.is_ok());
1497
1498 if let SponsoredTransactionQuoteResponse::Stellar(quote) = result.unwrap() {
1499 assert_eq!(quote.fee_in_token, "1500000");
1500 assert!(!quote.fee_in_token_ui.is_empty());
1501 assert!(!quote.conversion_rate.is_empty());
1502 } else {
1503 panic!("Expected Stellar quote response");
1504 }
1505 }
1506
1507 #[tokio::test]
1508 async fn test_quote_sponsored_transaction_with_operations() {
1509 let relayer_model = create_test_relayer_with_user_fee_strategy();
1510 let mut provider = MockStellarProviderTrait::new();
1511
1512 provider.expect_get_account().returning(|_| {
1513 Box::pin(ready(Ok(AccountEntry {
1514 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1515 balance: 1000000000,
1516 seq_num: SequenceNumber(-1),
1517 num_sub_entries: 0,
1518 inflation_dest: None,
1519 flags: 0,
1520 home_domain: String32::default(),
1521 thresholds: Thresholds([0; 4]),
1522 signers: VecM::default(),
1523 ext: AccountEntryExt::V0,
1524 })))
1525 });
1526
1527 provider.expect_get_ledger_entries().returning(|keys| {
1530 let account_id = if let Some(LedgerKey::Trustline(trustline_key)) = keys.first() {
1532 trustline_key.account_id.clone()
1533 } else {
1534 parse_account_id("GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2")
1536 .unwrap_or(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))))
1537 };
1538
1539 let issuer_id =
1540 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
1541 .unwrap_or(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))));
1542
1543 let trustline_entry = TrustLineEntry {
1545 account_id,
1546 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1547 asset_code: AssetCode4(*b"USDC"),
1548 issuer: issuer_id,
1549 }),
1550 balance: 10_000_000i64,
1551 limit: i64::MAX,
1552 flags: 0,
1553 ext: TrustLineEntryExt::V0,
1554 };
1555
1556 let ledger_entry = LedgerEntry {
1557 last_modified_ledger_seq: 0,
1558 data: LedgerEntryData::Trustline(trustline_entry),
1559 ext: LedgerEntryExt::V0,
1560 };
1561
1562 let xdr = ledger_entry
1564 .data
1565 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1566 .expect("Failed to encode trustline entry data to XDR");
1567
1568 Box::pin(ready(Ok(
1569 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
1570 entries: Some(vec![LedgerEntryResult {
1571 key: "test_key".to_string(),
1572 xdr,
1573 last_modified_ledger: 0u32,
1574 live_until_ledger_seq_ledger_seq: None,
1575 }]),
1576 latest_ledger: 0,
1577 },
1578 )))
1579 });
1580
1581 let mut dex_service = MockStellarDexServiceTrait::new();
1582 dex_service
1583 .expect_supported_asset_types()
1584 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1585
1586 dex_service
1588 .expect_get_xlm_to_token_quote()
1589 .returning(|_, _, _, _| {
1590 Box::pin(ready(Ok(
1591 crate::services::stellar_dex::StellarQuoteResponse {
1592 input_asset: "native".to_string(),
1593 output_asset: USDC_ASSET.to_string(),
1594 in_amount: 100000,
1595 out_amount: 1500000,
1596 price_impact_pct: 0.0,
1597 slippage_bps: 100,
1598 path: None,
1599 },
1600 )))
1601 });
1602
1603 let dex_service = Arc::new(dex_service);
1604 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1605
1606 let operations = vec![OperationSpec::Payment {
1607 destination: TEST_PK.to_string(),
1608 amount: 1000000,
1609 asset: AssetSpec::Native,
1610 }];
1611
1612 let request = SponsoredTransactionQuoteRequest::Stellar(
1613 crate::models::StellarFeeEstimateRequestParams {
1614 transaction_xdr: None,
1615 operations: Some(operations),
1616 source_account: Some(
1617 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2".to_string(),
1618 ),
1619 fee_token: USDC_ASSET.to_string(),
1620 },
1621 );
1622
1623 let result = relayer.quote_sponsored_transaction(request).await;
1624 if let Err(e) = &result {
1625 eprintln!("Quote error: {e:?}");
1626 }
1627 assert!(result.is_ok());
1628 }
1629
1630 #[tokio::test]
1631 async fn test_quote_sponsored_transaction_invalid_token() {
1632 let relayer_model = create_test_relayer_with_user_fee_strategy();
1633 let provider = MockStellarProviderTrait::new();
1634 let dex_service = create_mock_dex_service();
1635 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1636
1637 let transaction_xdr = create_test_transaction_xdr();
1638 let request = SponsoredTransactionQuoteRequest::Stellar(
1639 crate::models::StellarFeeEstimateRequestParams {
1640 transaction_xdr: Some(transaction_xdr),
1641 operations: None,
1642 source_account: None,
1643 fee_token: "INVALID:TOKEN".to_string(),
1644 },
1645 );
1646
1647 let result = relayer.quote_sponsored_transaction(request).await;
1648 assert!(result.is_err());
1649 assert!(matches!(
1650 result.unwrap_err(),
1651 RelayerError::ValidationError(_)
1652 ));
1653 }
1654
1655 #[tokio::test]
1656 async fn test_quote_sponsored_transaction_missing_xdr_and_operations() {
1657 let relayer_model = create_test_relayer_with_user_fee_strategy();
1658 let provider = MockStellarProviderTrait::new();
1659 let dex_service = create_mock_dex_service();
1660 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1661
1662 let request = SponsoredTransactionQuoteRequest::Stellar(
1663 crate::models::StellarFeeEstimateRequestParams {
1664 transaction_xdr: None,
1665 operations: None,
1666 source_account: None,
1667 fee_token: USDC_ASSET.to_string(),
1668 },
1669 );
1670
1671 let result = relayer.quote_sponsored_transaction(request).await;
1672 assert!(result.is_err());
1673 assert!(matches!(
1674 result.unwrap_err(),
1675 RelayerError::ValidationError(_)
1676 ));
1677 }
1678
1679 #[tokio::test]
1680 async fn test_build_sponsored_transaction_with_xdr() {
1681 let relayer_model = create_test_relayer_with_user_fee_strategy();
1682 let mut provider = MockStellarProviderTrait::new();
1683
1684 provider.expect_get_account().returning(|_| {
1685 Box::pin(ready(Ok(AccountEntry {
1686 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1687 balance: 1000000000,
1688 seq_num: SequenceNumber(-1),
1689 num_sub_entries: 0,
1690 inflation_dest: None,
1691 flags: 0,
1692 home_domain: String32::default(),
1693 thresholds: Thresholds([0; 4]),
1694 signers: VecM::default(),
1695 ext: AccountEntryExt::V0,
1696 })))
1697 });
1698
1699 provider.expect_get_ledger_entries().returning(|keys| {
1702 let account_id = if let Some(LedgerKey::Trustline(trustline_key)) = keys.first() {
1704 trustline_key.account_id.clone()
1705 } else {
1706 parse_account_id(TEST_PK)
1708 .unwrap_or(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))))
1709 };
1710
1711 let issuer_id =
1712 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
1713 .unwrap_or(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))));
1714
1715 let trustline_entry = TrustLineEntry {
1717 account_id,
1718 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1719 asset_code: AssetCode4(*b"USDC"),
1720 issuer: issuer_id,
1721 }),
1722 balance: 10_000_000i64, limit: i64::MAX,
1724 flags: 0,
1725 ext: TrustLineEntryExt::V0, };
1727
1728 let ledger_entry = LedgerEntry {
1729 last_modified_ledger_seq: 0,
1730 data: LedgerEntryData::Trustline(trustline_entry),
1731 ext: LedgerEntryExt::V0,
1732 };
1733
1734 let xdr = ledger_entry
1736 .data
1737 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1738 .expect("Failed to encode trustline entry data to XDR");
1739
1740 Box::pin(ready(Ok(
1741 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
1742 entries: Some(vec![LedgerEntryResult {
1743 key: "test_key".to_string(),
1744 xdr,
1745 last_modified_ledger: 0u32,
1746 live_until_ledger_seq_ledger_seq: None,
1747 }]),
1748 latest_ledger: 0,
1749 },
1750 )))
1751 });
1752
1753 let mut dex_service = MockStellarDexServiceTrait::new();
1754 dex_service
1755 .expect_supported_asset_types()
1756 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1757
1758 dex_service
1760 .expect_get_xlm_to_token_quote()
1761 .returning(|_, _, _, _| {
1762 Box::pin(ready(Ok(
1763 crate::services::stellar_dex::StellarQuoteResponse {
1764 input_asset: "native".to_string(),
1765 output_asset: USDC_ASSET.to_string(),
1766 in_amount: 1000000,
1767 out_amount: 1500000,
1768 price_impact_pct: 0.0,
1769 slippage_bps: 100,
1770 path: None,
1771 },
1772 )))
1773 });
1774
1775 let dex_service = Arc::new(dex_service);
1776 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1777
1778 let transaction_xdr = create_test_transaction_xdr();
1779 let request = SponsoredTransactionBuildRequest::Stellar(
1780 crate::models::StellarPrepareTransactionRequestParams {
1781 transaction_xdr: Some(transaction_xdr),
1782 operations: None,
1783 source_account: None,
1784 fee_token: USDC_ASSET.to_string(),
1785 },
1786 );
1787
1788 let result = relayer.build_sponsored_transaction(request).await;
1789 assert!(result.is_ok());
1790
1791 if let SponsoredTransactionBuildResponse::Stellar(build) = result.unwrap() {
1792 assert!(!build.transaction.is_empty());
1793 assert_eq!(build.fee_in_token, "1500000");
1794 assert!(!build.fee_in_token_ui.is_empty());
1795 assert_eq!(build.fee_token, USDC_ASSET);
1796 assert!(!build.valid_until.is_empty());
1797 } else {
1798 panic!("Expected Stellar build response");
1799 }
1800 }
1801
1802 #[tokio::test]
1803 async fn test_build_sponsored_transaction_with_operations() {
1804 let relayer_model = create_test_relayer_with_user_fee_strategy();
1805 let mut provider = MockStellarProviderTrait::new();
1806
1807 provider.expect_get_account().returning(|_| {
1808 Box::pin(ready(Ok(AccountEntry {
1809 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1810 balance: 1000000000,
1811 seq_num: SequenceNumber(-1),
1812 num_sub_entries: 0,
1813 inflation_dest: None,
1814 flags: 0,
1815 home_domain: String32::default(),
1816 thresholds: Thresholds([0; 4]),
1817 signers: VecM::default(),
1818 ext: AccountEntryExt::V0,
1819 })))
1820 });
1821
1822 provider.expect_get_ledger_entries().returning(|_| {
1823 use crate::domain::transaction::stellar::utils::parse_account_id;
1824 use soroban_rs::stellar_rpc_client::LedgerEntryResult;
1825 use soroban_rs::xdr::{
1826 AccountId, AlphaNum4, AssetCode4, LedgerEntry, LedgerEntryData, LedgerEntryExt,
1827 PublicKey, TrustLineEntry, TrustLineEntryExt, Uint256, WriteXdr,
1828 };
1829
1830 let account_id =
1832 parse_account_id("GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2")
1833 .unwrap_or(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))));
1834 let issuer_id =
1835 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
1836 .unwrap_or(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))));
1837
1838 let trustline_entry = TrustLineEntry {
1841 account_id,
1842 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1843 asset_code: AssetCode4(*b"USDC"),
1844 issuer: issuer_id,
1845 }),
1846 balance: 10_000_000i64,
1847 limit: i64::MAX,
1848 flags: 0,
1849 ext: TrustLineEntryExt::V0,
1850 };
1851
1852 let ledger_entry = LedgerEntry {
1853 last_modified_ledger_seq: 0,
1854 data: LedgerEntryData::Trustline(trustline_entry),
1855 ext: LedgerEntryExt::V0,
1856 };
1857
1858 let xdr = ledger_entry
1861 .data
1862 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1863 .expect("Failed to encode trustline entry data to XDR");
1864
1865 Box::pin(ready(Ok(
1866 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
1867 entries: Some(vec![LedgerEntryResult {
1868 key: "test_key".to_string(),
1869 xdr,
1870 last_modified_ledger: 0u32,
1871 live_until_ledger_seq_ledger_seq: None,
1872 }]),
1873 latest_ledger: 0,
1874 },
1875 )))
1876 });
1877
1878 let mut dex_service = MockStellarDexServiceTrait::new();
1879 dex_service
1880 .expect_supported_asset_types()
1881 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1882
1883 dex_service
1884 .expect_get_xlm_to_token_quote()
1885 .returning(|_, _, _, _| {
1886 Box::pin(ready(Ok(
1887 crate::services::stellar_dex::StellarQuoteResponse {
1888 input_asset: "native".to_string(),
1889 output_asset: USDC_ASSET.to_string(),
1890 in_amount: 1000000,
1891 out_amount: 1500000,
1892 price_impact_pct: 0.0,
1893 slippage_bps: 100,
1894 path: None,
1895 },
1896 )))
1897 });
1898
1899 let dex_service = Arc::new(dex_service);
1900 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1901
1902 let operations = vec![OperationSpec::Payment {
1903 destination: TEST_PK.to_string(),
1904 amount: 1000000,
1905 asset: AssetSpec::Native,
1906 }];
1907
1908 let request = SponsoredTransactionBuildRequest::Stellar(
1909 crate::models::StellarPrepareTransactionRequestParams {
1910 transaction_xdr: None,
1911 operations: Some(operations),
1912 source_account: Some(
1913 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2".to_string(),
1914 ),
1915 fee_token: USDC_ASSET.to_string(),
1916 },
1917 );
1918
1919 let result = relayer.build_sponsored_transaction(request).await;
1920
1921 assert!(result.is_ok());
1922 }
1923
1924 #[tokio::test]
1925 async fn test_build_sponsored_transaction_missing_source_account() {
1926 let relayer_model = create_test_relayer_with_user_fee_strategy();
1927 let provider = MockStellarProviderTrait::new();
1928 let dex_service = create_mock_dex_service();
1929 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1930
1931 let operations = vec![OperationSpec::Payment {
1932 destination: TEST_PK.to_string(),
1933 amount: 1000000,
1934 asset: AssetSpec::Native,
1935 }];
1936
1937 let request = SponsoredTransactionBuildRequest::Stellar(
1938 crate::models::StellarPrepareTransactionRequestParams {
1939 transaction_xdr: None,
1940 operations: Some(operations),
1941 source_account: None,
1942 fee_token: USDC_ASSET.to_string(),
1943 },
1944 );
1945
1946 let result = relayer.build_sponsored_transaction(request).await;
1947 assert!(result.is_err());
1948 assert!(matches!(
1949 result.unwrap_err(),
1950 RelayerError::ValidationError(_)
1951 ));
1952 }
1953
1954 #[tokio::test]
1955 async fn test_build_envelope_from_request_with_xdr() {
1956 let provider = MockStellarProviderTrait::new();
1957 let transaction_xdr = create_test_transaction_xdr();
1958 let result = build_envelope_from_request(
1959 Some(&transaction_xdr),
1960 None,
1961 None,
1962 TEST_NETWORK_PASSPHRASE,
1963 &provider,
1964 )
1965 .await;
1966 assert!(result.is_ok());
1967 }
1968
1969 #[tokio::test]
1970 async fn test_build_envelope_from_request_with_operations() {
1971 let mut provider = MockStellarProviderTrait::new();
1972
1973 provider.expect_get_account().returning(|_| {
1975 Box::pin(ready(Ok(AccountEntry {
1976 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1977 balance: 1000000000,
1978 seq_num: SequenceNumber(100),
1979 num_sub_entries: 0,
1980 inflation_dest: None,
1981 flags: 0,
1982 home_domain: String32::default(),
1983 thresholds: Thresholds([0; 4]),
1984 signers: VecM::default(),
1985 ext: AccountEntryExt::V0,
1986 })))
1987 });
1988
1989 let operations = vec![OperationSpec::Payment {
1990 destination: TEST_PK.to_string(),
1991 amount: 1000000,
1992 asset: AssetSpec::Native,
1993 }];
1994
1995 let result = build_envelope_from_request(
1996 None,
1997 Some(&operations),
1998 Some(&TEST_PK.to_string()),
1999 TEST_NETWORK_PASSPHRASE,
2000 &provider,
2001 )
2002 .await;
2003 assert!(result.is_ok());
2004
2005 if let Ok(envelope) = result {
2007 if let TransactionEnvelope::Tx(tx_env) = envelope {
2008 assert_eq!(tx_env.tx.seq_num.0, 101);
2009 }
2010 }
2011 }
2012
2013 #[tokio::test]
2014 async fn test_build_envelope_from_request_missing_source_account() {
2015 let provider = MockStellarProviderTrait::new();
2016 let operations = vec![OperationSpec::Payment {
2017 destination: TEST_PK.to_string(),
2018 amount: 1000000,
2019 asset: AssetSpec::Native,
2020 }];
2021
2022 let result = build_envelope_from_request(
2023 None,
2024 Some(&operations),
2025 None,
2026 TEST_NETWORK_PASSPHRASE,
2027 &provider,
2028 )
2029 .await;
2030 assert!(result.is_err());
2031 assert!(matches!(
2032 result.unwrap_err(),
2033 RelayerError::ValidationError(_)
2034 ));
2035 }
2036
2037 #[tokio::test]
2038 async fn test_build_envelope_from_request_missing_both() {
2039 let provider = MockStellarProviderTrait::new();
2040 let result =
2041 build_envelope_from_request(None, None, None, TEST_NETWORK_PASSPHRASE, &provider).await;
2042 assert!(result.is_err());
2043 assert!(matches!(
2044 result.unwrap_err(),
2045 RelayerError::ValidationError(_)
2046 ));
2047 }
2048
2049 #[tokio::test]
2050 async fn test_build_envelope_from_request_invalid_xdr() {
2051 let provider = MockStellarProviderTrait::new();
2052 let result = build_envelope_from_request(
2053 Some(&"INVALID_XDR".to_string()),
2054 None,
2055 None,
2056 TEST_NETWORK_PASSPHRASE,
2057 &provider,
2058 )
2059 .await;
2060 assert!(result.is_err());
2061 }
2062
2063 #[test]
2068 fn test_detect_soroban_invoke_from_xdr_classic_transaction() {
2069 let xdr = create_test_transaction_xdr();
2071 let result = detect_soroban_invoke_from_xdr(&xdr);
2072 assert!(result.is_ok());
2073 assert!(result.unwrap().is_none());
2074 }
2075
2076 #[test]
2077 fn test_detect_soroban_invoke_from_xdr_invalid_xdr() {
2078 let result = detect_soroban_invoke_from_xdr("INVALID_XDR");
2079 assert!(result.is_err());
2080 assert!(matches!(
2081 result.unwrap_err(),
2082 RelayerError::ValidationError(_)
2083 ));
2084 }
2085
2086 #[test]
2087 fn test_detect_soroban_invoke_from_xdr_with_soroban_transaction() {
2088 use soroban_rs::xdr::{
2089 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo,
2090 MuxedAccount, Operation, OperationBody, Preconditions, ScAddress, ScSymbol, ScVal,
2091 SequenceNumber, Transaction, TransactionEnvelope, TransactionExt,
2092 TransactionV1Envelope, Uint256, VecM,
2093 };
2094
2095 let contract_id = ContractId(Hash([1u8; 32]));
2097 let invoke_args = InvokeContractArgs {
2098 contract_address: ScAddress::Contract(contract_id),
2099 function_name: ScSymbol("test_function".try_into().unwrap()),
2100 args: vec![ScVal::Bool(true)].try_into().unwrap(),
2101 };
2102
2103 let invoke_op = InvokeHostFunctionOp {
2104 host_function: HostFunction::InvokeContract(invoke_args),
2105 auth: VecM::default(),
2106 };
2107
2108 let operation = Operation {
2109 source_account: None,
2110 body: OperationBody::InvokeHostFunction(invoke_op),
2111 };
2112
2113 let source_pk = Ed25519PublicKey::from_string(
2114 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2115 )
2116 .unwrap();
2117
2118 let tx = Transaction {
2119 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2120 fee: 100,
2121 seq_num: SequenceNumber(1),
2122 cond: Preconditions::None,
2123 memo: Memo::None,
2124 operations: vec![operation].try_into().unwrap(),
2125 ext: TransactionExt::V0,
2126 };
2127
2128 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2129 tx,
2130 signatures: VecM::default(),
2131 });
2132
2133 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2134 let result = detect_soroban_invoke_from_xdr(&xdr);
2135 assert!(result.is_ok());
2136
2137 let soroban_info = result.unwrap();
2138 assert!(soroban_info.is_some());
2139
2140 let info = soroban_info.unwrap();
2141 assert_eq!(info.target_fn, "test_function");
2142 assert_eq!(info.target_args.len(), 1);
2143 assert!(info.target_contract.starts_with('C'));
2145 }
2146
2147 #[test]
2148 fn test_detect_soroban_invoke_from_xdr_multiple_operations_error() {
2149 use soroban_rs::xdr::{
2150 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo,
2151 MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, ScAddress, ScSymbol,
2152 SequenceNumber, Transaction, TransactionEnvelope, TransactionExt,
2153 TransactionV1Envelope, Uint256, VecM,
2154 };
2155
2156 let contract_id = ContractId(Hash([1u8; 32]));
2158 let invoke_args = InvokeContractArgs {
2159 contract_address: ScAddress::Contract(contract_id),
2160 function_name: ScSymbol("test".try_into().unwrap()),
2161 args: VecM::default(),
2162 };
2163
2164 let invoke_op = InvokeHostFunctionOp {
2165 host_function: HostFunction::InvokeContract(invoke_args),
2166 auth: VecM::default(),
2167 };
2168
2169 let source_pk = Ed25519PublicKey::from_string(
2170 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2171 )
2172 .unwrap();
2173 let dest_pk = Ed25519PublicKey::from_string(
2174 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
2175 )
2176 .unwrap();
2177
2178 let payment_op = PaymentOp {
2179 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
2180 asset: soroban_rs::xdr::Asset::Native,
2181 amount: 1000000,
2182 };
2183
2184 let operations: VecM<Operation, 100> = vec![
2185 Operation {
2186 source_account: None,
2187 body: OperationBody::InvokeHostFunction(invoke_op),
2188 },
2189 Operation {
2190 source_account: None,
2191 body: OperationBody::Payment(payment_op),
2192 },
2193 ]
2194 .try_into()
2195 .unwrap();
2196
2197 let tx = Transaction {
2198 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2199 fee: 100,
2200 seq_num: SequenceNumber(1),
2201 cond: Preconditions::None,
2202 memo: Memo::None,
2203 operations,
2204 ext: TransactionExt::V0,
2205 };
2206
2207 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2208 tx,
2209 signatures: VecM::default(),
2210 });
2211
2212 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2213 let result = detect_soroban_invoke_from_xdr(&xdr);
2214
2215 assert!(result.is_err());
2216 let err = result.unwrap_err();
2217 assert!(matches!(err, RelayerError::ValidationError(_)));
2218 if let RelayerError::ValidationError(msg) = err {
2219 assert!(msg.contains("exactly one operation"));
2220 }
2221 }
2222
2223 #[test]
2224 fn test_detect_soroban_invoke_from_xdr_v0_envelope() {
2225 use soroban_rs::xdr::{
2226 Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TransactionEnvelope,
2227 TransactionV0, TransactionV0Envelope, TransactionV0Ext, Uint256, VecM,
2228 };
2229
2230 let source_pk = Ed25519PublicKey::from_string(
2232 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2233 )
2234 .unwrap();
2235 let dest_pk = Ed25519PublicKey::from_string(
2236 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
2237 )
2238 .unwrap();
2239
2240 let payment_op = PaymentOp {
2241 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
2242 asset: soroban_rs::xdr::Asset::Native,
2243 amount: 1000000,
2244 };
2245
2246 let tx = TransactionV0 {
2247 source_account_ed25519: Uint256(source_pk.0),
2248 fee: 100,
2249 seq_num: SequenceNumber(1),
2250 time_bounds: None,
2251 memo: Memo::None,
2252 operations: vec![Operation {
2253 source_account: None,
2254 body: OperationBody::Payment(payment_op),
2255 }]
2256 .try_into()
2257 .unwrap(),
2258 ext: TransactionV0Ext::V0,
2259 };
2260
2261 let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2262 tx,
2263 signatures: VecM::default(),
2264 });
2265
2266 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2267 let result = detect_soroban_invoke_from_xdr(&xdr);
2268
2269 assert!(result.is_ok());
2271 assert!(result.unwrap().is_none());
2272 }
2273
2274 #[test]
2275 fn test_detect_soroban_invoke_from_xdr_fee_bump_envelope() {
2276 use soroban_rs::xdr::{
2277 FeeBumpTransaction, FeeBumpTransactionEnvelope, FeeBumpTransactionExt,
2278 FeeBumpTransactionInnerTx, Memo, MuxedAccount, Operation, OperationBody, PaymentOp,
2279 Preconditions, SequenceNumber, Transaction, TransactionEnvelope, TransactionExt,
2280 TransactionV1Envelope, Uint256, VecM,
2281 };
2282
2283 let source_pk = Ed25519PublicKey::from_string(
2284 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2285 )
2286 .unwrap();
2287 let dest_pk = Ed25519PublicKey::from_string(
2288 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
2289 )
2290 .unwrap();
2291
2292 let payment_op = PaymentOp {
2293 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
2294 asset: soroban_rs::xdr::Asset::Native,
2295 amount: 1000000,
2296 };
2297
2298 let inner_tx = Transaction {
2299 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2300 fee: 100,
2301 seq_num: SequenceNumber(1),
2302 cond: Preconditions::None,
2303 memo: Memo::None,
2304 operations: vec![Operation {
2305 source_account: None,
2306 body: OperationBody::Payment(payment_op),
2307 }]
2308 .try_into()
2309 .unwrap(),
2310 ext: TransactionExt::V0,
2311 };
2312
2313 let inner_envelope = TransactionV1Envelope {
2314 tx: inner_tx,
2315 signatures: VecM::default(),
2316 };
2317
2318 let fee_bump_tx = FeeBumpTransaction {
2319 fee_source: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2320 fee: 200,
2321 inner_tx: FeeBumpTransactionInnerTx::Tx(inner_envelope),
2322 ext: FeeBumpTransactionExt::V0,
2323 };
2324
2325 let envelope = TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
2326 tx: fee_bump_tx,
2327 signatures: VecM::default(),
2328 });
2329
2330 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2331 let result = detect_soroban_invoke_from_xdr(&xdr);
2332
2333 assert!(result.is_ok());
2335 assert!(result.unwrap().is_none());
2336 }
2337
2338 #[test]
2339 fn test_detect_soroban_invoke_non_contract_address_error() {
2340 use soroban_rs::xdr::{
2341 HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo, MuxedAccount, Operation,
2342 OperationBody, Preconditions, ScAddress, ScSymbol, SequenceNumber, Transaction,
2343 TransactionEnvelope, TransactionExt, TransactionV1Envelope, Uint256, VecM,
2344 };
2345
2346 let source_pk = Ed25519PublicKey::from_string(
2348 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2349 )
2350 .unwrap();
2351
2352 let invoke_args = InvokeContractArgs {
2353 contract_address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
2354 Uint256(source_pk.0),
2355 ))),
2356 function_name: ScSymbol("test".try_into().unwrap()),
2357 args: VecM::default(),
2358 };
2359
2360 let invoke_op = InvokeHostFunctionOp {
2361 host_function: HostFunction::InvokeContract(invoke_args),
2362 auth: VecM::default(),
2363 };
2364
2365 let tx = Transaction {
2366 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2367 fee: 100,
2368 seq_num: SequenceNumber(1),
2369 cond: Preconditions::None,
2370 memo: Memo::None,
2371 operations: vec![Operation {
2372 source_account: None,
2373 body: OperationBody::InvokeHostFunction(invoke_op),
2374 }]
2375 .try_into()
2376 .unwrap(),
2377 ext: TransactionExt::V0,
2378 };
2379
2380 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2381 tx,
2382 signatures: VecM::default(),
2383 });
2384
2385 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2386 let result = detect_soroban_invoke_from_xdr(&xdr);
2387
2388 assert!(result.is_err());
2389 let err = result.unwrap_err();
2390 assert!(matches!(err, RelayerError::ValidationError(_)));
2391 if let RelayerError::ValidationError(msg) = err {
2392 assert!(msg.contains("contract address"));
2393 }
2394 }
2395
2396 #[test]
2401 fn test_calculate_total_soroban_fee_success() {
2402 let sim_response = soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2403 error: None,
2404 transaction_data: "".to_string(),
2405 min_resource_fee: 50000,
2406 ..Default::default()
2407 };
2408
2409 let result = calculate_total_soroban_fee(&sim_response, 1);
2410 assert!(result.is_ok());
2411 let fee = result.unwrap();
2413 assert_eq!(fee, 50100);
2414 }
2415
2416 #[test]
2417 fn test_calculate_total_soroban_fee_with_multiple_operations() {
2418 let sim_response = soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2419 error: None,
2420 transaction_data: "".to_string(),
2421 min_resource_fee: 50000,
2422 ..Default::default()
2423 };
2424
2425 let result = calculate_total_soroban_fee(&sim_response, 3);
2426 assert!(result.is_ok());
2427 let fee = result.unwrap();
2429 assert_eq!(fee, 50300);
2430 }
2431
2432 #[test]
2433 fn test_calculate_total_soroban_fee_simulation_error() {
2434 let sim_response = soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2435 error: Some("Simulation failed: insufficient funds".to_string()),
2436 transaction_data: "".to_string(),
2437 min_resource_fee: 0,
2438 ..Default::default()
2439 };
2440
2441 let result = calculate_total_soroban_fee(&sim_response, 1);
2442 assert!(result.is_err());
2443 let err = result.unwrap_err();
2444 assert!(matches!(err, RelayerError::ValidationError(_)));
2445 if let RelayerError::ValidationError(msg) = err {
2446 assert!(msg.contains("Simulation failed"));
2447 }
2448 }
2449
2450 #[test]
2451 fn test_calculate_total_soroban_fee_minimum_fee() {
2452 let sim_response = soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2454 error: None,
2455 transaction_data: "".to_string(),
2456 min_resource_fee: 0, ..Default::default()
2458 };
2459
2460 let result = calculate_total_soroban_fee(&sim_response, 1);
2461 assert!(result.is_ok());
2462 let fee = result.unwrap();
2464 assert!(fee >= STELLAR_DEFAULT_TRANSACTION_FEE);
2465 }
2466
2467 #[test]
2472 fn test_build_soroban_transaction_envelope_success() {
2473 use soroban_rs::xdr::{
2474 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Operation,
2475 OperationBody, ScAddress, ScSymbol, VecM,
2476 };
2477
2478 let contract_id = ContractId(Hash([1u8; 32]));
2479 let invoke_args = InvokeContractArgs {
2480 contract_address: ScAddress::Contract(contract_id),
2481 function_name: ScSymbol("test".try_into().unwrap()),
2482 args: VecM::default(),
2483 };
2484
2485 let invoke_op = InvokeHostFunctionOp {
2486 host_function: HostFunction::InvokeContract(invoke_args),
2487 auth: VecM::default(),
2488 };
2489
2490 let operation = Operation {
2491 source_account: None,
2492 body: OperationBody::InvokeHostFunction(invoke_op),
2493 };
2494
2495 let result = build_soroban_transaction_envelope(TEST_PK, operation.clone(), 100);
2496 assert!(result.is_ok());
2497
2498 let envelope = result.unwrap();
2499 if let TransactionEnvelope::Tx(tx_env) = envelope {
2500 assert_eq!(tx_env.tx.fee, 100);
2501 assert_eq!(tx_env.tx.seq_num.0, 0); assert_eq!(tx_env.tx.operations.len(), 1);
2503 } else {
2504 panic!("Expected Tx envelope");
2505 }
2506 }
2507
2508 #[test]
2509 fn test_build_soroban_transaction_envelope_invalid_source() {
2510 use soroban_rs::xdr::{
2511 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Operation,
2512 OperationBody, ScAddress, ScSymbol, VecM,
2513 };
2514
2515 let contract_id = ContractId(Hash([1u8; 32]));
2516 let invoke_args = InvokeContractArgs {
2517 contract_address: ScAddress::Contract(contract_id),
2518 function_name: ScSymbol("test".try_into().unwrap()),
2519 args: VecM::default(),
2520 };
2521
2522 let invoke_op = InvokeHostFunctionOp {
2523 host_function: HostFunction::InvokeContract(invoke_args),
2524 auth: VecM::default(),
2525 };
2526
2527 let operation = Operation {
2528 source_account: None,
2529 body: OperationBody::InvokeHostFunction(invoke_op),
2530 };
2531
2532 let result = build_soroban_transaction_envelope("INVALID_ADDRESS", operation, 100);
2533 assert!(result.is_err());
2534 assert!(matches!(
2535 result.unwrap_err(),
2536 RelayerError::ValidationError(_)
2537 ));
2538 }
2539
2540 #[test]
2545 fn test_add_payment_operation_to_envelope_classic() {
2546 let envelope = create_test_envelope_for_payment();
2547 let fee_quote = FeeQuote {
2548 fee_in_token: 1000000,
2549 fee_in_token_ui: "1.0".to_string(),
2550 fee_in_stroops: 10000,
2551 conversion_rate: 100.0,
2552 };
2553
2554 let result = add_payment_operation_to_envelope(envelope, &fee_quote, USDC_ASSET, TEST_PK);
2555 assert!(result.is_ok());
2556
2557 let updated_envelope = result.unwrap();
2558 if let TransactionEnvelope::Tx(tx_env) = updated_envelope {
2560 assert_eq!(tx_env.tx.operations.len(), 2);
2561 }
2562 }
2563
2564 #[test]
2565 fn test_add_payment_operation_to_envelope_soroban_no_op_added() {
2566 use soroban_rs::xdr::{
2567 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo,
2568 Operation, OperationBody, Preconditions, ScAddress, ScSymbol, SequenceNumber,
2569 Transaction, TransactionEnvelope, TransactionExt, TransactionV1Envelope, Uint256, VecM,
2570 };
2571
2572 let source_pk = Ed25519PublicKey::from_string(
2574 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2575 )
2576 .unwrap();
2577
2578 let contract_id = ContractId(Hash([1u8; 32]));
2579 let invoke_args = InvokeContractArgs {
2580 contract_address: ScAddress::Contract(contract_id),
2581 function_name: ScSymbol("test".try_into().unwrap()),
2582 args: VecM::default(),
2583 };
2584
2585 let invoke_op = InvokeHostFunctionOp {
2586 host_function: HostFunction::InvokeContract(invoke_args),
2587 auth: VecM::default(),
2588 };
2589
2590 let tx = Transaction {
2591 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2592 fee: 100,
2593 seq_num: SequenceNumber(1),
2594 cond: Preconditions::None,
2595 memo: Memo::None,
2596 operations: vec![Operation {
2597 source_account: None,
2598 body: OperationBody::InvokeHostFunction(invoke_op),
2599 }]
2600 .try_into()
2601 .unwrap(),
2602 ext: TransactionExt::V0,
2603 };
2604
2605 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2606 tx,
2607 signatures: VecM::default(),
2608 });
2609
2610 let fee_quote = FeeQuote {
2611 fee_in_token: 1000000,
2612 fee_in_token_ui: "1.0".to_string(),
2613 fee_in_stroops: 10000,
2614 conversion_rate: 100.0,
2615 };
2616
2617 let result = add_payment_operation_to_envelope(envelope, &fee_quote, USDC_ASSET, TEST_PK);
2618 assert!(result.is_ok());
2619
2620 let updated_envelope = result.unwrap();
2622 if let TransactionEnvelope::Tx(tx_env) = updated_envelope {
2623 assert_eq!(tx_env.tx.operations.len(), 1); }
2625 }
2626
2627 fn create_test_envelope_for_payment() -> TransactionEnvelope {
2629 let source_pk = Ed25519PublicKey::from_string(
2630 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2631 )
2632 .unwrap();
2633 let dest_pk = Ed25519PublicKey::from_string(
2634 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
2635 )
2636 .unwrap();
2637
2638 let payment_op = PaymentOp {
2639 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
2640 asset: soroban_rs::xdr::Asset::Native,
2641 amount: 1000000,
2642 };
2643
2644 let tx = Transaction {
2645 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2646 fee: 100,
2647 seq_num: SequenceNumber(1),
2648 cond: Preconditions::None,
2649 memo: soroban_rs::xdr::Memo::None,
2650 operations: vec![Operation {
2651 source_account: None,
2652 body: OperationBody::Payment(payment_op),
2653 }]
2654 .try_into()
2655 .unwrap(),
2656 ext: TransactionExt::V0,
2657 };
2658
2659 TransactionEnvelope::Tx(TransactionV1Envelope {
2660 tx,
2661 signatures: VecM::default(),
2662 })
2663 }
2664
2665 #[test]
2670 fn test_add_fee_payment_operation_success() {
2671 let mut envelope = create_test_envelope_for_payment();
2672 let result = add_fee_payment_operation(&mut envelope, USDC_ASSET, 1000000, TEST_PK);
2673 assert!(result.is_ok());
2674
2675 if let TransactionEnvelope::Tx(tx_env) = envelope {
2677 assert_eq!(tx_env.tx.operations.len(), 2);
2678 }
2679 }
2680
2681 #[test]
2682 fn test_add_fee_payment_operation_native_asset() {
2683 let mut envelope = create_test_envelope_for_payment();
2684 let result = add_fee_payment_operation(&mut envelope, "native", 1000000, TEST_PK);
2685 assert!(result.is_ok());
2686 }
2687
2688 #[test]
2693 fn test_soroban_invoke_info_debug_clone() {
2694 use soroban_rs::xdr::ScVal;
2695
2696 let info = SorobanInvokeInfo {
2697 target_contract: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
2698 target_fn: "transfer".to_string(),
2699 target_args: vec![ScVal::Bool(true)],
2700 };
2701
2702 let debug_str = format!("{:?}", info);
2704 assert!(debug_str.contains("SorobanInvokeInfo"));
2705 assert!(debug_str.contains("transfer"));
2706
2707 let cloned = info.clone();
2709 assert_eq!(cloned.target_contract, info.target_contract);
2710 assert_eq!(cloned.target_fn, info.target_fn);
2711 assert_eq!(cloned.target_args.len(), info.target_args.len());
2712 }
2713
2714 #[tokio::test]
2719 async fn test_build_sponsored_transaction_non_user_fee_strategy() {
2720 let mut policy = RelayerStellarPolicy::default();
2722 policy.fee_payment_strategy = Some(crate::models::StellarFeePaymentStrategy::Relayer);
2723 policy.allowed_tokens = Some(vec![crate::models::StellarAllowedTokensPolicy {
2724 asset: USDC_ASSET.to_string(),
2725 metadata: None,
2726 max_allowed_fee: None,
2727 swap_config: None,
2728 }]);
2729
2730 let relayer_model = RelayerRepoModel {
2731 id: "test-relayer-id".to_string(),
2732 name: "Test Relayer".to_string(),
2733 network: "testnet".to_string(),
2734 paused: false,
2735 network_type: NetworkType::Stellar,
2736 signer_id: "signer-id".to_string(),
2737 policies: RelayerNetworkPolicy::Stellar(policy),
2738 address: TEST_PK.to_string(),
2739 notification_id: Some("notification-id".to_string()),
2740 system_disabled: false,
2741 custom_rpc_urls: None,
2742 ..Default::default()
2743 };
2744
2745 let provider = MockStellarProviderTrait::new();
2746 let dex_service = create_mock_dex_service();
2747 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
2748
2749 let transaction_xdr = create_test_transaction_xdr();
2750 let request = SponsoredTransactionBuildRequest::Stellar(
2751 crate::models::StellarPrepareTransactionRequestParams {
2752 transaction_xdr: Some(transaction_xdr),
2753 operations: None,
2754 source_account: None,
2755 fee_token: USDC_ASSET.to_string(),
2756 },
2757 );
2758
2759 let result = relayer.build_sponsored_transaction(request).await;
2760 assert!(result.is_err());
2761 let err = result.unwrap_err();
2762 assert!(matches!(err, RelayerError::ValidationError(_)));
2763 if let RelayerError::ValidationError(msg) = err {
2764 assert!(msg.contains("fee_payment_strategy: User"));
2765 }
2766 }
2767
2768 fn create_valid_soroban_transaction_data_xdr() -> String {
2774 use soroban_rs::xdr::{
2775 LedgerFootprint, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
2776 };
2777
2778 let soroban_data = SorobanTransactionData {
2779 ext: SorobanTransactionDataExt::V0,
2780 resources: SorobanResources {
2781 footprint: LedgerFootprint {
2782 read_only: VecM::default(),
2783 read_write: VecM::default(),
2784 },
2785 instructions: 1000000,
2786 disk_read_bytes: 10000,
2787 write_bytes: 1000,
2788 },
2789 resource_fee: 50000,
2790 };
2791
2792 soroban_data.to_xdr_base64(Limits::none()).unwrap()
2793 }
2794
2795 fn create_test_soroban_transaction_xdr() -> String {
2797 use soroban_rs::xdr::{
2798 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo,
2799 ScAddress, ScSymbol, ScVal,
2800 };
2801
2802 let source_pk = Ed25519PublicKey::from_string(
2803 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2804 )
2805 .unwrap();
2806
2807 let contract_id = ContractId(Hash([1u8; 32]));
2809 let invoke_args = InvokeContractArgs {
2810 contract_address: ScAddress::Contract(contract_id),
2811 function_name: ScSymbol("transfer".try_into().unwrap()),
2812 args: vec![ScVal::Bool(true)].try_into().unwrap(),
2813 };
2814
2815 let invoke_op = InvokeHostFunctionOp {
2816 host_function: HostFunction::InvokeContract(invoke_args),
2817 auth: VecM::default(),
2818 };
2819
2820 let operation = Operation {
2821 source_account: None,
2822 body: OperationBody::InvokeHostFunction(invoke_op),
2823 };
2824
2825 let tx = Transaction {
2826 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2827 fee: 100,
2828 seq_num: SequenceNumber(1),
2829 cond: Preconditions::None,
2830 memo: Memo::None,
2831 operations: vec![operation].try_into().unwrap(),
2832 ext: TransactionExt::V0,
2833 };
2834
2835 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2836 tx,
2837 signatures: VecM::default(),
2838 });
2839
2840 envelope.to_xdr_base64(Limits::none()).unwrap()
2841 }
2842
2843 fn create_test_relayer_with_soroban_token() -> RelayerRepoModel {
2845 let mut policy = RelayerStellarPolicy::default();
2846 policy.fee_payment_strategy = Some(crate::models::StellarFeePaymentStrategy::User);
2847 policy.allowed_tokens = Some(vec![crate::models::StellarAllowedTokensPolicy {
2849 asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
2850 metadata: None,
2851 max_allowed_fee: None,
2852 swap_config: None,
2853 }]);
2854
2855 RelayerRepoModel {
2856 id: "test-relayer-id".to_string(),
2857 name: "Test Relayer".to_string(),
2858 network: "testnet".to_string(),
2859 paused: false,
2860 network_type: NetworkType::Stellar,
2861 signer_id: "signer-id".to_string(),
2862 policies: RelayerNetworkPolicy::Stellar(policy),
2863 address: TEST_PK.to_string(),
2864 notification_id: Some("notification-id".to_string()),
2865 system_disabled: false,
2866 custom_rpc_urls: None,
2867 ..Default::default()
2868 }
2869 }
2870
2871 #[tokio::test]
2872 #[serial]
2873 async fn test_quote_soroban_from_xdr_success() {
2874 std::env::set_var(
2876 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
2877 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
2878 );
2879
2880 let relayer_model = create_test_relayer_with_soroban_token();
2881 let mut provider = MockStellarProviderTrait::new();
2882
2883 provider.expect_get_latest_ledger().returning(|| {
2885 Box::pin(ready(Ok(
2886 soroban_rs::stellar_rpc_client::GetLatestLedgerResponse {
2887 id: "test".to_string(),
2888 protocol_version: 20,
2889 sequence: 1000,
2890 },
2891 )))
2892 });
2893
2894 provider
2896 .expect_simulate_transaction_envelope()
2897 .returning(|_| {
2898 Box::pin(ready(Ok(
2899 soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2900 min_resource_fee: 50000,
2901 transaction_data: "AAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAABgAAAAEAAAAGAAAAAG0JZTO9fU6p3NeJp5w3TpKhZmx6p1pR7mq9wFwCnEIuAAAAFAAAAAEAAAAAAAAAB8NVb2IAAAH0AAAAAQAAAAAAABfAAAAAAAAAAPUAAAAAAAAENgAAAAA=".to_string(),
2902 ..Default::default()
2903 },
2904 )))
2905 });
2906
2907 provider.expect_call_contract().returning(|_, _, _| {
2909 use soroban_rs::xdr::Int128Parts;
2910 Box::pin(ready(Ok(ScVal::I128(Int128Parts {
2912 hi: 0,
2913 lo: 10_000_000,
2914 }))))
2915 });
2916
2917 let mut dex_service = MockStellarDexServiceTrait::new();
2918 dex_service.expect_supported_asset_types().returning(|| {
2919 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
2920 });
2921
2922 dex_service
2924 .expect_get_xlm_to_token_quote()
2925 .returning(|_, _, _, _| {
2926 Box::pin(ready(Ok(
2927 crate::services::stellar_dex::StellarQuoteResponse {
2928 input_asset: "native".to_string(),
2929 output_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
2930 .to_string(),
2931 in_amount: 50100, out_amount: 1500000, price_impact_pct: 0.0,
2934 slippage_bps: 100,
2935 path: None,
2936 },
2937 )))
2938 });
2939
2940 let dex_service = Arc::new(dex_service);
2941 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
2942
2943 let transaction_xdr = create_test_soroban_transaction_xdr();
2944 let request = SponsoredTransactionQuoteRequest::Stellar(
2945 crate::models::StellarFeeEstimateRequestParams {
2946 transaction_xdr: Some(transaction_xdr),
2947 operations: None,
2948 source_account: None,
2949 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
2950 },
2951 );
2952
2953 let result = relayer.quote_sponsored_transaction(request).await;
2954 if let Err(e) = &result {
2955 eprintln!("Soroban quote error: {:?}", e);
2956 }
2957 assert!(result.is_ok());
2958
2959 if let SponsoredTransactionQuoteResponse::Stellar(quote) = result.unwrap() {
2960 assert_eq!(quote.fee_in_token, "1500000");
2961 assert!(!quote.fee_in_token_ui.is_empty());
2962 assert!(!quote.conversion_rate.is_empty());
2963 } else {
2964 panic!("Expected Stellar quote response");
2965 }
2966
2967 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
2969 }
2970
2971 #[tokio::test]
2972 #[serial]
2973 async fn test_quote_soroban_from_xdr_missing_fee_forwarder() {
2974 std::env::remove_var("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS");
2976
2977 let mut relayer_model = create_test_relayer_with_soroban_token();
2979 relayer_model.network = "mainnet".to_string();
2980
2981 let provider = MockStellarProviderTrait::new();
2982
2983 let mut dex_service = MockStellarDexServiceTrait::new();
2984 dex_service.expect_supported_asset_types().returning(|| {
2985 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
2986 });
2987
2988 let dex_service = Arc::new(dex_service);
2989 let relayer = create_test_relayer_instance_with_network(
2990 relayer_model,
2991 provider,
2992 dex_service,
2993 create_test_mainnet_network(),
2994 )
2995 .await;
2996
2997 let transaction_xdr = create_test_soroban_transaction_xdr();
2998 let request = SponsoredTransactionQuoteRequest::Stellar(
2999 crate::models::StellarFeeEstimateRequestParams {
3000 transaction_xdr: Some(transaction_xdr),
3001 operations: None,
3002 source_account: None,
3003 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3004 },
3005 );
3006
3007 let result = relayer.quote_sponsored_transaction(request).await;
3008 assert!(result.is_err());
3009 let err = result.unwrap_err();
3010 assert!(matches!(err, RelayerError::ValidationError(_)));
3011 if let RelayerError::ValidationError(msg) = err {
3012 assert!(msg.contains("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS"));
3013 }
3014 }
3015
3016 #[tokio::test]
3017 #[serial]
3018 async fn test_quote_soroban_from_xdr_invalid_fee_token_format() {
3019 std::env::set_var(
3021 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
3022 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
3023 );
3024
3025 let mut policy = RelayerStellarPolicy::default();
3027 policy.fee_payment_strategy = Some(crate::models::StellarFeePaymentStrategy::User);
3028 policy.allowed_tokens = Some(vec![
3029 crate::models::StellarAllowedTokensPolicy {
3030 asset: USDC_ASSET.to_string(), metadata: None,
3032 max_allowed_fee: None,
3033 swap_config: None,
3034 },
3035 crate::models::StellarAllowedTokensPolicy {
3036 asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3037 metadata: None,
3038 max_allowed_fee: None,
3039 swap_config: None,
3040 },
3041 ]);
3042
3043 let relayer_model = RelayerRepoModel {
3044 id: "test-relayer-id".to_string(),
3045 name: "Test Relayer".to_string(),
3046 network: "testnet".to_string(),
3047 paused: false,
3048 network_type: NetworkType::Stellar,
3049 signer_id: "signer-id".to_string(),
3050 policies: RelayerNetworkPolicy::Stellar(policy),
3051 address: TEST_PK.to_string(),
3052 notification_id: Some("notification-id".to_string()),
3053 system_disabled: false,
3054 custom_rpc_urls: None,
3055 ..Default::default()
3056 };
3057
3058 let provider = MockStellarProviderTrait::new();
3059
3060 let mut dex_service = MockStellarDexServiceTrait::new();
3061 dex_service
3062 .expect_supported_asset_types()
3063 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
3064
3065 let dex_service = Arc::new(dex_service);
3066 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
3067
3068 let transaction_xdr = create_test_soroban_transaction_xdr();
3070 let request = SponsoredTransactionQuoteRequest::Stellar(
3071 crate::models::StellarFeeEstimateRequestParams {
3072 transaction_xdr: Some(transaction_xdr),
3073 operations: None,
3074 source_account: None,
3075 fee_token: USDC_ASSET.to_string(), },
3077 );
3078
3079 let result = relayer.quote_sponsored_transaction(request).await;
3080 assert!(result.is_err());
3081 let err = result.unwrap_err();
3082 assert!(matches!(err, RelayerError::ValidationError(_)));
3083 if let RelayerError::ValidationError(msg) = err {
3084 assert!(msg.contains("Soroban contract address"));
3085 }
3086
3087 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
3089 }
3090
3091 #[tokio::test]
3096 #[serial]
3097 async fn test_build_soroban_sponsored_success() {
3098 std::env::set_var(
3100 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
3101 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
3102 );
3103
3104 let relayer_model = create_test_relayer_with_soroban_token();
3105 let mut provider = MockStellarProviderTrait::new();
3106
3107 provider.expect_get_latest_ledger().returning(|| {
3109 Box::pin(ready(Ok(
3110 soroban_rs::stellar_rpc_client::GetLatestLedgerResponse {
3111 id: "test".to_string(),
3112 protocol_version: 20,
3113 sequence: 1000,
3114 },
3115 )))
3116 });
3117
3118 let valid_tx_data = create_valid_soroban_transaction_data_xdr();
3120 provider
3121 .expect_simulate_transaction_envelope()
3122 .returning(move |_| {
3123 let tx_data = valid_tx_data.clone();
3124 Box::pin(ready(Ok(
3125 soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
3126 min_resource_fee: 50000,
3127 transaction_data: tx_data,
3128 ..Default::default()
3129 },
3130 )))
3131 });
3132
3133 provider.expect_call_contract().returning(|_, _, _| {
3135 use soroban_rs::xdr::Int128Parts;
3136 Box::pin(ready(Ok(ScVal::I128(Int128Parts {
3138 hi: 0,
3139 lo: 10_000_000,
3140 }))))
3141 });
3142
3143 let mut dex_service = MockStellarDexServiceTrait::new();
3144 dex_service.expect_supported_asset_types().returning(|| {
3145 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
3146 });
3147
3148 dex_service
3150 .expect_get_xlm_to_token_quote()
3151 .returning(|_, _, _, _| {
3152 Box::pin(ready(Ok(
3153 crate::services::stellar_dex::StellarQuoteResponse {
3154 input_asset: "native".to_string(),
3155 output_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
3156 .to_string(),
3157 in_amount: 50100,
3158 out_amount: 1500000,
3159 price_impact_pct: 0.0,
3160 slippage_bps: 100,
3161 path: None,
3162 },
3163 )))
3164 });
3165
3166 let dex_service = Arc::new(dex_service);
3167 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
3168
3169 let transaction_xdr = create_test_soroban_transaction_xdr();
3170 let request = SponsoredTransactionBuildRequest::Stellar(
3171 crate::models::StellarPrepareTransactionRequestParams {
3172 transaction_xdr: Some(transaction_xdr),
3173 operations: None,
3174 source_account: None,
3175 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3176 },
3177 );
3178
3179 let result = relayer.build_sponsored_transaction(request).await;
3180 if let Err(e) = &result {
3181 eprintln!("Soroban build error: {:?}", e);
3182 }
3183 assert!(result.is_ok());
3184
3185 if let SponsoredTransactionBuildResponse::Stellar(build) = result.unwrap() {
3186 assert!(!build.transaction.is_empty());
3187 assert_eq!(build.fee_in_token, "1500000");
3188 assert!(!build.fee_in_token_ui.is_empty());
3189 assert_eq!(
3190 build.fee_token,
3191 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
3192 );
3193 assert!(!build.valid_until.is_empty());
3194 assert!(build.user_auth_entry.is_some());
3196 assert!(!build.user_auth_entry.unwrap().is_empty());
3197 } else {
3198 panic!("Expected Stellar build response");
3199 }
3200
3201 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
3203 }
3204
3205 #[tokio::test]
3206 #[serial]
3207 async fn test_build_soroban_sponsored_missing_fee_forwarder() {
3208 std::env::remove_var("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS");
3210
3211 let mut relayer_model = create_test_relayer_with_soroban_token();
3213 relayer_model.network = "mainnet".to_string();
3214
3215 let provider = MockStellarProviderTrait::new();
3216
3217 let mut dex_service = MockStellarDexServiceTrait::new();
3218 dex_service.expect_supported_asset_types().returning(|| {
3219 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
3220 });
3221
3222 let dex_service = Arc::new(dex_service);
3223 let relayer = create_test_relayer_instance_with_network(
3224 relayer_model,
3225 provider,
3226 dex_service,
3227 create_test_mainnet_network(),
3228 )
3229 .await;
3230
3231 let transaction_xdr = create_test_soroban_transaction_xdr();
3232 let request = SponsoredTransactionBuildRequest::Stellar(
3233 crate::models::StellarPrepareTransactionRequestParams {
3234 transaction_xdr: Some(transaction_xdr),
3235 operations: None,
3236 source_account: None,
3237 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3238 },
3239 );
3240
3241 let result = relayer.build_sponsored_transaction(request).await;
3242 assert!(result.is_err());
3243 let err = result.unwrap_err();
3244 assert!(matches!(err, RelayerError::ValidationError(_)));
3245 if let RelayerError::ValidationError(msg) = err {
3246 assert!(msg.contains("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS"));
3247 }
3248 }
3249
3250 #[tokio::test]
3251 #[serial]
3252 async fn test_build_soroban_sponsored_insufficient_balance() {
3253 std::env::set_var(
3255 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
3256 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
3257 );
3258
3259 let relayer_model = create_test_relayer_with_soroban_token();
3260 let mut provider = MockStellarProviderTrait::new();
3261
3262 provider.expect_get_latest_ledger().returning(|| {
3264 Box::pin(ready(Ok(
3265 soroban_rs::stellar_rpc_client::GetLatestLedgerResponse {
3266 id: "test".to_string(),
3267 protocol_version: 20,
3268 sequence: 1000,
3269 },
3270 )))
3271 });
3272
3273 provider
3275 .expect_simulate_transaction_envelope()
3276 .returning(|_| {
3277 Box::pin(ready(Ok(
3278 soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
3279 min_resource_fee: 50000,
3280 transaction_data: "AAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAABgAAAAEAAAAGAAAAAG0JZTO9fU6p3NeJp5w3TpKhZmx6p1pR7mq9wFwCnEIuAAAAFAAAAAEAAAAAAAAAB8NVb2IAAAH0AAAAAQAAAAAAABfAAAAAAAAAAPUAAAAAAAAENgAAAAA=".to_string(),
3281 ..Default::default()
3282 },
3283 )))
3284 });
3285
3286 provider.expect_call_contract().returning(|_, _, _| {
3288 use soroban_rs::xdr::Int128Parts;
3289 Box::pin(ready(Ok(ScVal::I128(Int128Parts { hi: 0, lo: 100 }))))
3291 });
3292
3293 let mut dex_service = MockStellarDexServiceTrait::new();
3294 dex_service.expect_supported_asset_types().returning(|| {
3295 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
3296 });
3297
3298 dex_service
3300 .expect_get_xlm_to_token_quote()
3301 .returning(|_, _, _, _| {
3302 Box::pin(ready(Ok(
3303 crate::services::stellar_dex::StellarQuoteResponse {
3304 input_asset: "native".to_string(),
3305 output_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
3306 .to_string(),
3307 in_amount: 50100,
3308 out_amount: 1500000, price_impact_pct: 0.0,
3310 slippage_bps: 100,
3311 path: None,
3312 },
3313 )))
3314 });
3315
3316 let dex_service = Arc::new(dex_service);
3317 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
3318
3319 let transaction_xdr = create_test_soroban_transaction_xdr();
3320 let request = SponsoredTransactionBuildRequest::Stellar(
3321 crate::models::StellarPrepareTransactionRequestParams {
3322 transaction_xdr: Some(transaction_xdr),
3323 operations: None,
3324 source_account: None,
3325 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3326 },
3327 );
3328
3329 let result = relayer.build_sponsored_transaction(request).await;
3330 assert!(result.is_err());
3331 let err = result.unwrap_err();
3332 assert!(matches!(err, RelayerError::ValidationError(_)));
3333 if let RelayerError::ValidationError(msg) = err {
3334 assert!(msg.contains("Insufficient balance"));
3335 }
3336
3337 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
3339 }
3340
3341 #[tokio::test]
3342 #[serial]
3343 async fn test_build_soroban_sponsored_simulation_error() {
3344 std::env::set_var(
3346 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
3347 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
3348 );
3349
3350 let relayer_model = create_test_relayer_with_soroban_token();
3351 let mut provider = MockStellarProviderTrait::new();
3352
3353 provider.expect_get_latest_ledger().returning(|| {
3355 Box::pin(ready(Ok(
3356 soroban_rs::stellar_rpc_client::GetLatestLedgerResponse {
3357 id: "test".to_string(),
3358 protocol_version: 20,
3359 sequence: 1000,
3360 },
3361 )))
3362 });
3363
3364 provider
3366 .expect_simulate_transaction_envelope()
3367 .returning(|_| {
3368 Box::pin(ready(Ok(
3369 soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
3370 error: Some(
3371 "Contract execution failed: insufficient resources".to_string(),
3372 ),
3373 min_resource_fee: 0,
3374 transaction_data: "".to_string(),
3375 ..Default::default()
3376 },
3377 )))
3378 });
3379
3380 let mut dex_service = MockStellarDexServiceTrait::new();
3381 dex_service.expect_supported_asset_types().returning(|| {
3382 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
3383 });
3384
3385 dex_service
3387 .expect_get_xlm_to_token_quote()
3388 .returning(|_, _, _, _| {
3389 Box::pin(ready(Ok(
3390 crate::services::stellar_dex::StellarQuoteResponse {
3391 input_asset: "native".to_string(),
3392 output_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
3393 .to_string(),
3394 in_amount: 100,
3395 out_amount: 1500,
3396 price_impact_pct: 0.0,
3397 slippage_bps: 100,
3398 path: None,
3399 },
3400 )))
3401 });
3402
3403 let dex_service = Arc::new(dex_service);
3404 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
3405
3406 let transaction_xdr = create_test_soroban_transaction_xdr();
3407 let request = SponsoredTransactionBuildRequest::Stellar(
3408 crate::models::StellarPrepareTransactionRequestParams {
3409 transaction_xdr: Some(transaction_xdr),
3410 operations: None,
3411 source_account: None,
3412 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3413 },
3414 );
3415
3416 let result = relayer.build_sponsored_transaction(request).await;
3417 assert!(result.is_err());
3418 let err = result.unwrap_err();
3419 assert!(matches!(err, RelayerError::ValidationError(_)));
3421 if let RelayerError::ValidationError(msg) = err {
3422 assert!(msg.contains("Simulation failed"));
3423 }
3424
3425 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
3427 }
3428}