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).unwrap_or_else(|_| {
1414 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1415 })
1416 };
1417
1418 let issuer_id =
1419 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
1420 .unwrap_or_else(|_| {
1421 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1422 });
1423
1424 let trustline_entry = TrustLineEntry {
1426 account_id,
1427 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1428 asset_code: AssetCode4(*b"USDC"),
1429 issuer: issuer_id,
1430 }),
1431 balance: 10_000_000i64,
1432 limit: i64::MAX,
1433 flags: 0,
1434 ext: TrustLineEntryExt::V0,
1435 };
1436
1437 let ledger_entry = LedgerEntry {
1438 last_modified_ledger_seq: 0,
1439 data: LedgerEntryData::Trustline(trustline_entry),
1440 ext: LedgerEntryExt::V0,
1441 };
1442
1443 let xdr = ledger_entry
1445 .data
1446 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1447 .expect("Failed to encode trustline entry data to XDR");
1448
1449 Box::pin(ready(Ok(GetLedgerEntriesResponse {
1450 entries: Some(vec![LedgerEntryResult {
1451 key: "test_key".to_string(),
1452 xdr,
1453 last_modified_ledger: 0u32,
1454 live_until_ledger_seq_ledger_seq: None,
1455 }]),
1456 latest_ledger: 0,
1457 })))
1458 });
1459
1460 let mut dex_service = MockStellarDexServiceTrait::new();
1461 dex_service
1462 .expect_supported_asset_types()
1463 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1464
1465 dex_service
1467 .expect_get_xlm_to_token_quote()
1468 .returning(|_, _, _, _| {
1469 Box::pin(ready(Ok(
1470 crate::services::stellar_dex::StellarQuoteResponse {
1471 input_asset: "native".to_string(),
1472 output_asset: USDC_ASSET.to_string(),
1473 in_amount: 100000,
1474 out_amount: 1500000,
1475 price_impact_pct: 0.0,
1476 slippage_bps: 100,
1477 path: None,
1478 },
1479 )))
1480 });
1481
1482 let dex_service = Arc::new(dex_service);
1483 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1484
1485 let transaction_xdr = create_test_transaction_xdr();
1486 let request = SponsoredTransactionQuoteRequest::Stellar(
1487 crate::models::StellarFeeEstimateRequestParams {
1488 transaction_xdr: Some(transaction_xdr),
1489 operations: None,
1490 source_account: None,
1491 fee_token: USDC_ASSET.to_string(),
1492 },
1493 );
1494
1495 let result = relayer.quote_sponsored_transaction(request).await;
1496 if let Err(e) = &result {
1497 eprintln!("Quote error: {:?}", e);
1498 }
1499 assert!(result.is_ok());
1500
1501 if let SponsoredTransactionQuoteResponse::Stellar(quote) = result.unwrap() {
1502 assert_eq!(quote.fee_in_token, "1500000");
1503 assert!(!quote.fee_in_token_ui.is_empty());
1504 assert!(!quote.conversion_rate.is_empty());
1505 } else {
1506 panic!("Expected Stellar quote response");
1507 }
1508 }
1509
1510 #[tokio::test]
1511 async fn test_quote_sponsored_transaction_with_operations() {
1512 let relayer_model = create_test_relayer_with_user_fee_strategy();
1513 let mut provider = MockStellarProviderTrait::new();
1514
1515 provider.expect_get_account().returning(|_| {
1516 Box::pin(ready(Ok(AccountEntry {
1517 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1518 balance: 1000000000,
1519 seq_num: SequenceNumber(-1),
1520 num_sub_entries: 0,
1521 inflation_dest: None,
1522 flags: 0,
1523 home_domain: String32::default(),
1524 thresholds: Thresholds([0; 4]),
1525 signers: VecM::default(),
1526 ext: AccountEntryExt::V0,
1527 })))
1528 });
1529
1530 provider.expect_get_ledger_entries().returning(|keys| {
1533 let account_id = if let Some(LedgerKey::Trustline(trustline_key)) = keys.first() {
1535 trustline_key.account_id.clone()
1536 } else {
1537 parse_account_id("GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2")
1539 .unwrap_or_else(|_| {
1540 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1541 })
1542 };
1543
1544 let issuer_id =
1545 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
1546 .unwrap_or_else(|_| {
1547 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1548 });
1549
1550 let trustline_entry = TrustLineEntry {
1552 account_id,
1553 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1554 asset_code: AssetCode4(*b"USDC"),
1555 issuer: issuer_id,
1556 }),
1557 balance: 10_000_000i64,
1558 limit: i64::MAX,
1559 flags: 0,
1560 ext: TrustLineEntryExt::V0,
1561 };
1562
1563 let ledger_entry = LedgerEntry {
1564 last_modified_ledger_seq: 0,
1565 data: LedgerEntryData::Trustline(trustline_entry),
1566 ext: LedgerEntryExt::V0,
1567 };
1568
1569 let xdr = ledger_entry
1571 .data
1572 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1573 .expect("Failed to encode trustline entry data to XDR");
1574
1575 Box::pin(ready(Ok(
1576 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
1577 entries: Some(vec![LedgerEntryResult {
1578 key: "test_key".to_string(),
1579 xdr,
1580 last_modified_ledger: 0u32,
1581 live_until_ledger_seq_ledger_seq: None,
1582 }]),
1583 latest_ledger: 0,
1584 },
1585 )))
1586 });
1587
1588 let mut dex_service = MockStellarDexServiceTrait::new();
1589 dex_service
1590 .expect_supported_asset_types()
1591 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1592
1593 dex_service
1595 .expect_get_xlm_to_token_quote()
1596 .returning(|_, _, _, _| {
1597 Box::pin(ready(Ok(
1598 crate::services::stellar_dex::StellarQuoteResponse {
1599 input_asset: "native".to_string(),
1600 output_asset: USDC_ASSET.to_string(),
1601 in_amount: 100000,
1602 out_amount: 1500000,
1603 price_impact_pct: 0.0,
1604 slippage_bps: 100,
1605 path: None,
1606 },
1607 )))
1608 });
1609
1610 let dex_service = Arc::new(dex_service);
1611 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1612
1613 let operations = vec![OperationSpec::Payment {
1614 destination: TEST_PK.to_string(),
1615 amount: 1000000,
1616 asset: AssetSpec::Native,
1617 }];
1618
1619 let request = SponsoredTransactionQuoteRequest::Stellar(
1620 crate::models::StellarFeeEstimateRequestParams {
1621 transaction_xdr: None,
1622 operations: Some(operations),
1623 source_account: Some(
1624 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2".to_string(),
1625 ),
1626 fee_token: USDC_ASSET.to_string(),
1627 },
1628 );
1629
1630 let result = relayer.quote_sponsored_transaction(request).await;
1631 if let Err(e) = &result {
1632 eprintln!("Quote error: {:?}", e);
1633 }
1634 assert!(result.is_ok());
1635 }
1636
1637 #[tokio::test]
1638 async fn test_quote_sponsored_transaction_invalid_token() {
1639 let relayer_model = create_test_relayer_with_user_fee_strategy();
1640 let provider = MockStellarProviderTrait::new();
1641 let dex_service = create_mock_dex_service();
1642 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1643
1644 let transaction_xdr = create_test_transaction_xdr();
1645 let request = SponsoredTransactionQuoteRequest::Stellar(
1646 crate::models::StellarFeeEstimateRequestParams {
1647 transaction_xdr: Some(transaction_xdr),
1648 operations: None,
1649 source_account: None,
1650 fee_token: "INVALID:TOKEN".to_string(),
1651 },
1652 );
1653
1654 let result = relayer.quote_sponsored_transaction(request).await;
1655 assert!(result.is_err());
1656 assert!(matches!(
1657 result.unwrap_err(),
1658 RelayerError::ValidationError(_)
1659 ));
1660 }
1661
1662 #[tokio::test]
1663 async fn test_quote_sponsored_transaction_missing_xdr_and_operations() {
1664 let relayer_model = create_test_relayer_with_user_fee_strategy();
1665 let provider = MockStellarProviderTrait::new();
1666 let dex_service = create_mock_dex_service();
1667 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1668
1669 let request = SponsoredTransactionQuoteRequest::Stellar(
1670 crate::models::StellarFeeEstimateRequestParams {
1671 transaction_xdr: None,
1672 operations: None,
1673 source_account: None,
1674 fee_token: USDC_ASSET.to_string(),
1675 },
1676 );
1677
1678 let result = relayer.quote_sponsored_transaction(request).await;
1679 assert!(result.is_err());
1680 assert!(matches!(
1681 result.unwrap_err(),
1682 RelayerError::ValidationError(_)
1683 ));
1684 }
1685
1686 #[tokio::test]
1687 async fn test_build_sponsored_transaction_with_xdr() {
1688 let relayer_model = create_test_relayer_with_user_fee_strategy();
1689 let mut provider = MockStellarProviderTrait::new();
1690
1691 provider.expect_get_account().returning(|_| {
1692 Box::pin(ready(Ok(AccountEntry {
1693 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1694 balance: 1000000000,
1695 seq_num: SequenceNumber(-1),
1696 num_sub_entries: 0,
1697 inflation_dest: None,
1698 flags: 0,
1699 home_domain: String32::default(),
1700 thresholds: Thresholds([0; 4]),
1701 signers: VecM::default(),
1702 ext: AccountEntryExt::V0,
1703 })))
1704 });
1705
1706 provider.expect_get_ledger_entries().returning(|keys| {
1709 let account_id = if let Some(LedgerKey::Trustline(trustline_key)) = keys.first() {
1711 trustline_key.account_id.clone()
1712 } else {
1713 parse_account_id(TEST_PK).unwrap_or_else(|_| {
1715 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1716 })
1717 };
1718
1719 let issuer_id =
1720 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
1721 .unwrap_or_else(|_| {
1722 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1723 });
1724
1725 let trustline_entry = TrustLineEntry {
1727 account_id,
1728 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1729 asset_code: AssetCode4(*b"USDC"),
1730 issuer: issuer_id,
1731 }),
1732 balance: 10_000_000i64, limit: i64::MAX,
1734 flags: 0,
1735 ext: TrustLineEntryExt::V0, };
1737
1738 let ledger_entry = LedgerEntry {
1739 last_modified_ledger_seq: 0,
1740 data: LedgerEntryData::Trustline(trustline_entry),
1741 ext: LedgerEntryExt::V0,
1742 };
1743
1744 let xdr = ledger_entry
1746 .data
1747 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1748 .expect("Failed to encode trustline entry data to XDR");
1749
1750 Box::pin(ready(Ok(
1751 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
1752 entries: Some(vec![LedgerEntryResult {
1753 key: "test_key".to_string(),
1754 xdr,
1755 last_modified_ledger: 0u32,
1756 live_until_ledger_seq_ledger_seq: None,
1757 }]),
1758 latest_ledger: 0,
1759 },
1760 )))
1761 });
1762
1763 let mut dex_service = MockStellarDexServiceTrait::new();
1764 dex_service
1765 .expect_supported_asset_types()
1766 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1767
1768 dex_service
1770 .expect_get_xlm_to_token_quote()
1771 .returning(|_, _, _, _| {
1772 Box::pin(ready(Ok(
1773 crate::services::stellar_dex::StellarQuoteResponse {
1774 input_asset: "native".to_string(),
1775 output_asset: USDC_ASSET.to_string(),
1776 in_amount: 1000000,
1777 out_amount: 1500000,
1778 price_impact_pct: 0.0,
1779 slippage_bps: 100,
1780 path: None,
1781 },
1782 )))
1783 });
1784
1785 let dex_service = Arc::new(dex_service);
1786 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1787
1788 let transaction_xdr = create_test_transaction_xdr();
1789 let request = SponsoredTransactionBuildRequest::Stellar(
1790 crate::models::StellarPrepareTransactionRequestParams {
1791 transaction_xdr: Some(transaction_xdr),
1792 operations: None,
1793 source_account: None,
1794 fee_token: USDC_ASSET.to_string(),
1795 },
1796 );
1797
1798 let result = relayer.build_sponsored_transaction(request).await;
1799 assert!(result.is_ok());
1800
1801 if let SponsoredTransactionBuildResponse::Stellar(build) = result.unwrap() {
1802 assert!(!build.transaction.is_empty());
1803 assert_eq!(build.fee_in_token, "1500000");
1804 assert!(!build.fee_in_token_ui.is_empty());
1805 assert_eq!(build.fee_token, USDC_ASSET);
1806 assert!(!build.valid_until.is_empty());
1807 } else {
1808 panic!("Expected Stellar build response");
1809 }
1810 }
1811
1812 #[tokio::test]
1813 async fn test_build_sponsored_transaction_with_operations() {
1814 let relayer_model = create_test_relayer_with_user_fee_strategy();
1815 let mut provider = MockStellarProviderTrait::new();
1816
1817 provider.expect_get_account().returning(|_| {
1818 Box::pin(ready(Ok(AccountEntry {
1819 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1820 balance: 1000000000,
1821 seq_num: SequenceNumber(-1),
1822 num_sub_entries: 0,
1823 inflation_dest: None,
1824 flags: 0,
1825 home_domain: String32::default(),
1826 thresholds: Thresholds([0; 4]),
1827 signers: VecM::default(),
1828 ext: AccountEntryExt::V0,
1829 })))
1830 });
1831
1832 provider.expect_get_ledger_entries().returning(|_| {
1833 use crate::domain::transaction::stellar::utils::parse_account_id;
1834 use soroban_rs::stellar_rpc_client::LedgerEntryResult;
1835 use soroban_rs::xdr::{
1836 AccountId, AlphaNum4, AssetCode4, LedgerEntry, LedgerEntryData, LedgerEntryExt,
1837 PublicKey, TrustLineEntry, TrustLineEntryExt, Uint256, WriteXdr,
1838 };
1839
1840 let account_id =
1842 parse_account_id("GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2")
1843 .unwrap_or_else(|_| {
1844 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1845 });
1846 let issuer_id =
1847 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
1848 .unwrap_or_else(|_| {
1849 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1850 });
1851
1852 let trustline_entry = TrustLineEntry {
1855 account_id,
1856 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1857 asset_code: AssetCode4(*b"USDC"),
1858 issuer: issuer_id,
1859 }),
1860 balance: 10_000_000i64,
1861 limit: i64::MAX,
1862 flags: 0,
1863 ext: TrustLineEntryExt::V0,
1864 };
1865
1866 let ledger_entry = LedgerEntry {
1867 last_modified_ledger_seq: 0,
1868 data: LedgerEntryData::Trustline(trustline_entry),
1869 ext: LedgerEntryExt::V0,
1870 };
1871
1872 let xdr = ledger_entry
1875 .data
1876 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1877 .expect("Failed to encode trustline entry data to XDR");
1878
1879 Box::pin(ready(Ok(
1880 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
1881 entries: Some(vec![LedgerEntryResult {
1882 key: "test_key".to_string(),
1883 xdr,
1884 last_modified_ledger: 0u32,
1885 live_until_ledger_seq_ledger_seq: None,
1886 }]),
1887 latest_ledger: 0,
1888 },
1889 )))
1890 });
1891
1892 let mut dex_service = MockStellarDexServiceTrait::new();
1893 dex_service
1894 .expect_supported_asset_types()
1895 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1896
1897 dex_service
1898 .expect_get_xlm_to_token_quote()
1899 .returning(|_, _, _, _| {
1900 Box::pin(ready(Ok(
1901 crate::services::stellar_dex::StellarQuoteResponse {
1902 input_asset: "native".to_string(),
1903 output_asset: USDC_ASSET.to_string(),
1904 in_amount: 1000000,
1905 out_amount: 1500000,
1906 price_impact_pct: 0.0,
1907 slippage_bps: 100,
1908 path: None,
1909 },
1910 )))
1911 });
1912
1913 let dex_service = Arc::new(dex_service);
1914 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1915
1916 let operations = vec![OperationSpec::Payment {
1917 destination: TEST_PK.to_string(),
1918 amount: 1000000,
1919 asset: AssetSpec::Native,
1920 }];
1921
1922 let request = SponsoredTransactionBuildRequest::Stellar(
1923 crate::models::StellarPrepareTransactionRequestParams {
1924 transaction_xdr: None,
1925 operations: Some(operations),
1926 source_account: Some(
1927 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2".to_string(),
1928 ),
1929 fee_token: USDC_ASSET.to_string(),
1930 },
1931 );
1932
1933 let result = relayer.build_sponsored_transaction(request).await;
1934
1935 assert!(result.is_ok());
1936 }
1937
1938 #[tokio::test]
1939 async fn test_build_sponsored_transaction_missing_source_account() {
1940 let relayer_model = create_test_relayer_with_user_fee_strategy();
1941 let provider = MockStellarProviderTrait::new();
1942 let dex_service = create_mock_dex_service();
1943 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1944
1945 let operations = vec![OperationSpec::Payment {
1946 destination: TEST_PK.to_string(),
1947 amount: 1000000,
1948 asset: AssetSpec::Native,
1949 }];
1950
1951 let request = SponsoredTransactionBuildRequest::Stellar(
1952 crate::models::StellarPrepareTransactionRequestParams {
1953 transaction_xdr: None,
1954 operations: Some(operations),
1955 source_account: None,
1956 fee_token: USDC_ASSET.to_string(),
1957 },
1958 );
1959
1960 let result = relayer.build_sponsored_transaction(request).await;
1961 assert!(result.is_err());
1962 assert!(matches!(
1963 result.unwrap_err(),
1964 RelayerError::ValidationError(_)
1965 ));
1966 }
1967
1968 #[tokio::test]
1969 async fn test_build_envelope_from_request_with_xdr() {
1970 let provider = MockStellarProviderTrait::new();
1971 let transaction_xdr = create_test_transaction_xdr();
1972 let result = build_envelope_from_request(
1973 Some(&transaction_xdr),
1974 None,
1975 None,
1976 TEST_NETWORK_PASSPHRASE,
1977 &provider,
1978 )
1979 .await;
1980 assert!(result.is_ok());
1981 }
1982
1983 #[tokio::test]
1984 async fn test_build_envelope_from_request_with_operations() {
1985 let mut provider = MockStellarProviderTrait::new();
1986
1987 provider.expect_get_account().returning(|_| {
1989 Box::pin(ready(Ok(AccountEntry {
1990 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1991 balance: 1000000000,
1992 seq_num: SequenceNumber(100),
1993 num_sub_entries: 0,
1994 inflation_dest: None,
1995 flags: 0,
1996 home_domain: String32::default(),
1997 thresholds: Thresholds([0; 4]),
1998 signers: VecM::default(),
1999 ext: AccountEntryExt::V0,
2000 })))
2001 });
2002
2003 let operations = vec![OperationSpec::Payment {
2004 destination: TEST_PK.to_string(),
2005 amount: 1000000,
2006 asset: AssetSpec::Native,
2007 }];
2008
2009 let result = build_envelope_from_request(
2010 None,
2011 Some(&operations),
2012 Some(&TEST_PK.to_string()),
2013 TEST_NETWORK_PASSPHRASE,
2014 &provider,
2015 )
2016 .await;
2017 assert!(result.is_ok());
2018
2019 if let Ok(envelope) = result {
2021 if let TransactionEnvelope::Tx(tx_env) = envelope {
2022 assert_eq!(tx_env.tx.seq_num.0, 101);
2023 }
2024 }
2025 }
2026
2027 #[tokio::test]
2028 async fn test_build_envelope_from_request_missing_source_account() {
2029 let provider = MockStellarProviderTrait::new();
2030 let operations = vec![OperationSpec::Payment {
2031 destination: TEST_PK.to_string(),
2032 amount: 1000000,
2033 asset: AssetSpec::Native,
2034 }];
2035
2036 let result = build_envelope_from_request(
2037 None,
2038 Some(&operations),
2039 None,
2040 TEST_NETWORK_PASSPHRASE,
2041 &provider,
2042 )
2043 .await;
2044 assert!(result.is_err());
2045 assert!(matches!(
2046 result.unwrap_err(),
2047 RelayerError::ValidationError(_)
2048 ));
2049 }
2050
2051 #[tokio::test]
2052 async fn test_build_envelope_from_request_missing_both() {
2053 let provider = MockStellarProviderTrait::new();
2054 let result =
2055 build_envelope_from_request(None, None, None, TEST_NETWORK_PASSPHRASE, &provider).await;
2056 assert!(result.is_err());
2057 assert!(matches!(
2058 result.unwrap_err(),
2059 RelayerError::ValidationError(_)
2060 ));
2061 }
2062
2063 #[tokio::test]
2064 async fn test_build_envelope_from_request_invalid_xdr() {
2065 let provider = MockStellarProviderTrait::new();
2066 let result = build_envelope_from_request(
2067 Some(&"INVALID_XDR".to_string()),
2068 None,
2069 None,
2070 TEST_NETWORK_PASSPHRASE,
2071 &provider,
2072 )
2073 .await;
2074 assert!(result.is_err());
2075 }
2076
2077 #[test]
2082 fn test_detect_soroban_invoke_from_xdr_classic_transaction() {
2083 let xdr = create_test_transaction_xdr();
2085 let result = detect_soroban_invoke_from_xdr(&xdr);
2086 assert!(result.is_ok());
2087 assert!(result.unwrap().is_none());
2088 }
2089
2090 #[test]
2091 fn test_detect_soroban_invoke_from_xdr_invalid_xdr() {
2092 let result = detect_soroban_invoke_from_xdr("INVALID_XDR");
2093 assert!(result.is_err());
2094 assert!(matches!(
2095 result.unwrap_err(),
2096 RelayerError::ValidationError(_)
2097 ));
2098 }
2099
2100 #[test]
2101 fn test_detect_soroban_invoke_from_xdr_with_soroban_transaction() {
2102 use soroban_rs::xdr::{
2103 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo,
2104 MuxedAccount, Operation, OperationBody, Preconditions, ScAddress, ScSymbol, ScVal,
2105 SequenceNumber, Transaction, TransactionEnvelope, TransactionExt,
2106 TransactionV1Envelope, Uint256, VecM,
2107 };
2108
2109 let contract_id = ContractId(Hash([1u8; 32]));
2111 let invoke_args = InvokeContractArgs {
2112 contract_address: ScAddress::Contract(contract_id),
2113 function_name: ScSymbol("test_function".try_into().unwrap()),
2114 args: vec![ScVal::Bool(true)].try_into().unwrap(),
2115 };
2116
2117 let invoke_op = InvokeHostFunctionOp {
2118 host_function: HostFunction::InvokeContract(invoke_args),
2119 auth: VecM::default(),
2120 };
2121
2122 let operation = Operation {
2123 source_account: None,
2124 body: OperationBody::InvokeHostFunction(invoke_op),
2125 };
2126
2127 let source_pk = Ed25519PublicKey::from_string(
2128 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2129 )
2130 .unwrap();
2131
2132 let tx = Transaction {
2133 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2134 fee: 100,
2135 seq_num: SequenceNumber(1),
2136 cond: Preconditions::None,
2137 memo: Memo::None,
2138 operations: vec![operation].try_into().unwrap(),
2139 ext: TransactionExt::V0,
2140 };
2141
2142 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2143 tx,
2144 signatures: VecM::default(),
2145 });
2146
2147 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2148 let result = detect_soroban_invoke_from_xdr(&xdr);
2149 assert!(result.is_ok());
2150
2151 let soroban_info = result.unwrap();
2152 assert!(soroban_info.is_some());
2153
2154 let info = soroban_info.unwrap();
2155 assert_eq!(info.target_fn, "test_function");
2156 assert_eq!(info.target_args.len(), 1);
2157 assert!(info.target_contract.starts_with('C'));
2159 }
2160
2161 #[test]
2162 fn test_detect_soroban_invoke_from_xdr_multiple_operations_error() {
2163 use soroban_rs::xdr::{
2164 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo,
2165 MuxedAccount, Operation, OperationBody, PaymentOp, Preconditions, ScAddress, ScSymbol,
2166 SequenceNumber, Transaction, TransactionEnvelope, TransactionExt,
2167 TransactionV1Envelope, Uint256, VecM,
2168 };
2169
2170 let contract_id = ContractId(Hash([1u8; 32]));
2172 let invoke_args = InvokeContractArgs {
2173 contract_address: ScAddress::Contract(contract_id),
2174 function_name: ScSymbol("test".try_into().unwrap()),
2175 args: VecM::default(),
2176 };
2177
2178 let invoke_op = InvokeHostFunctionOp {
2179 host_function: HostFunction::InvokeContract(invoke_args),
2180 auth: VecM::default(),
2181 };
2182
2183 let source_pk = Ed25519PublicKey::from_string(
2184 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2185 )
2186 .unwrap();
2187 let dest_pk = Ed25519PublicKey::from_string(
2188 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
2189 )
2190 .unwrap();
2191
2192 let payment_op = PaymentOp {
2193 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
2194 asset: soroban_rs::xdr::Asset::Native,
2195 amount: 1000000,
2196 };
2197
2198 let operations: VecM<Operation, 100> = vec![
2199 Operation {
2200 source_account: None,
2201 body: OperationBody::InvokeHostFunction(invoke_op),
2202 },
2203 Operation {
2204 source_account: None,
2205 body: OperationBody::Payment(payment_op),
2206 },
2207 ]
2208 .try_into()
2209 .unwrap();
2210
2211 let tx = Transaction {
2212 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2213 fee: 100,
2214 seq_num: SequenceNumber(1),
2215 cond: Preconditions::None,
2216 memo: Memo::None,
2217 operations,
2218 ext: TransactionExt::V0,
2219 };
2220
2221 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2222 tx,
2223 signatures: VecM::default(),
2224 });
2225
2226 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2227 let result = detect_soroban_invoke_from_xdr(&xdr);
2228
2229 assert!(result.is_err());
2230 let err = result.unwrap_err();
2231 assert!(matches!(err, RelayerError::ValidationError(_)));
2232 if let RelayerError::ValidationError(msg) = err {
2233 assert!(msg.contains("exactly one operation"));
2234 }
2235 }
2236
2237 #[test]
2238 fn test_detect_soroban_invoke_from_xdr_v0_envelope() {
2239 use soroban_rs::xdr::{
2240 Memo, Operation, OperationBody, PaymentOp, SequenceNumber, TransactionEnvelope,
2241 TransactionV0, TransactionV0Envelope, TransactionV0Ext, Uint256, VecM,
2242 };
2243
2244 let source_pk = Ed25519PublicKey::from_string(
2246 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2247 )
2248 .unwrap();
2249 let dest_pk = Ed25519PublicKey::from_string(
2250 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
2251 )
2252 .unwrap();
2253
2254 let payment_op = PaymentOp {
2255 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
2256 asset: soroban_rs::xdr::Asset::Native,
2257 amount: 1000000,
2258 };
2259
2260 let tx = TransactionV0 {
2261 source_account_ed25519: Uint256(source_pk.0),
2262 fee: 100,
2263 seq_num: SequenceNumber(1),
2264 time_bounds: None,
2265 memo: Memo::None,
2266 operations: vec![Operation {
2267 source_account: None,
2268 body: OperationBody::Payment(payment_op),
2269 }]
2270 .try_into()
2271 .unwrap(),
2272 ext: TransactionV0Ext::V0,
2273 };
2274
2275 let envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
2276 tx,
2277 signatures: VecM::default(),
2278 });
2279
2280 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2281 let result = detect_soroban_invoke_from_xdr(&xdr);
2282
2283 assert!(result.is_ok());
2285 assert!(result.unwrap().is_none());
2286 }
2287
2288 #[test]
2289 fn test_detect_soroban_invoke_from_xdr_fee_bump_envelope() {
2290 use soroban_rs::xdr::{
2291 FeeBumpTransaction, FeeBumpTransactionEnvelope, FeeBumpTransactionExt,
2292 FeeBumpTransactionInnerTx, Memo, MuxedAccount, Operation, OperationBody, PaymentOp,
2293 Preconditions, SequenceNumber, Transaction, TransactionEnvelope, TransactionExt,
2294 TransactionV1Envelope, Uint256, VecM,
2295 };
2296
2297 let source_pk = Ed25519PublicKey::from_string(
2298 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2299 )
2300 .unwrap();
2301 let dest_pk = Ed25519PublicKey::from_string(
2302 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
2303 )
2304 .unwrap();
2305
2306 let payment_op = PaymentOp {
2307 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
2308 asset: soroban_rs::xdr::Asset::Native,
2309 amount: 1000000,
2310 };
2311
2312 let inner_tx = Transaction {
2313 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2314 fee: 100,
2315 seq_num: SequenceNumber(1),
2316 cond: Preconditions::None,
2317 memo: Memo::None,
2318 operations: vec![Operation {
2319 source_account: None,
2320 body: OperationBody::Payment(payment_op),
2321 }]
2322 .try_into()
2323 .unwrap(),
2324 ext: TransactionExt::V0,
2325 };
2326
2327 let inner_envelope = TransactionV1Envelope {
2328 tx: inner_tx,
2329 signatures: VecM::default(),
2330 };
2331
2332 let fee_bump_tx = FeeBumpTransaction {
2333 fee_source: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2334 fee: 200,
2335 inner_tx: FeeBumpTransactionInnerTx::Tx(inner_envelope),
2336 ext: FeeBumpTransactionExt::V0,
2337 };
2338
2339 let envelope = TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
2340 tx: fee_bump_tx,
2341 signatures: VecM::default(),
2342 });
2343
2344 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2345 let result = detect_soroban_invoke_from_xdr(&xdr);
2346
2347 assert!(result.is_ok());
2349 assert!(result.unwrap().is_none());
2350 }
2351
2352 #[test]
2353 fn test_detect_soroban_invoke_non_contract_address_error() {
2354 use soroban_rs::xdr::{
2355 HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo, MuxedAccount, Operation,
2356 OperationBody, Preconditions, ScAddress, ScSymbol, SequenceNumber, Transaction,
2357 TransactionEnvelope, TransactionExt, TransactionV1Envelope, Uint256, VecM,
2358 };
2359
2360 let source_pk = Ed25519PublicKey::from_string(
2362 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2363 )
2364 .unwrap();
2365
2366 let invoke_args = InvokeContractArgs {
2367 contract_address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
2368 Uint256(source_pk.0),
2369 ))),
2370 function_name: ScSymbol("test".try_into().unwrap()),
2371 args: VecM::default(),
2372 };
2373
2374 let invoke_op = InvokeHostFunctionOp {
2375 host_function: HostFunction::InvokeContract(invoke_args),
2376 auth: VecM::default(),
2377 };
2378
2379 let tx = Transaction {
2380 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2381 fee: 100,
2382 seq_num: SequenceNumber(1),
2383 cond: Preconditions::None,
2384 memo: Memo::None,
2385 operations: vec![Operation {
2386 source_account: None,
2387 body: OperationBody::InvokeHostFunction(invoke_op),
2388 }]
2389 .try_into()
2390 .unwrap(),
2391 ext: TransactionExt::V0,
2392 };
2393
2394 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2395 tx,
2396 signatures: VecM::default(),
2397 });
2398
2399 let xdr = envelope.to_xdr_base64(Limits::none()).unwrap();
2400 let result = detect_soroban_invoke_from_xdr(&xdr);
2401
2402 assert!(result.is_err());
2403 let err = result.unwrap_err();
2404 assert!(matches!(err, RelayerError::ValidationError(_)));
2405 if let RelayerError::ValidationError(msg) = err {
2406 assert!(msg.contains("contract address"));
2407 }
2408 }
2409
2410 #[test]
2415 fn test_calculate_total_soroban_fee_success() {
2416 let sim_response = soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2417 error: None,
2418 transaction_data: "".to_string(),
2419 min_resource_fee: 50000,
2420 ..Default::default()
2421 };
2422
2423 let result = calculate_total_soroban_fee(&sim_response, 1);
2424 assert!(result.is_ok());
2425 let fee = result.unwrap();
2427 assert_eq!(fee, 50100);
2428 }
2429
2430 #[test]
2431 fn test_calculate_total_soroban_fee_with_multiple_operations() {
2432 let sim_response = soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2433 error: None,
2434 transaction_data: "".to_string(),
2435 min_resource_fee: 50000,
2436 ..Default::default()
2437 };
2438
2439 let result = calculate_total_soroban_fee(&sim_response, 3);
2440 assert!(result.is_ok());
2441 let fee = result.unwrap();
2443 assert_eq!(fee, 50300);
2444 }
2445
2446 #[test]
2447 fn test_calculate_total_soroban_fee_simulation_error() {
2448 let sim_response = soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2449 error: Some("Simulation failed: insufficient funds".to_string()),
2450 transaction_data: "".to_string(),
2451 min_resource_fee: 0,
2452 ..Default::default()
2453 };
2454
2455 let result = calculate_total_soroban_fee(&sim_response, 1);
2456 assert!(result.is_err());
2457 let err = result.unwrap_err();
2458 assert!(matches!(err, RelayerError::ValidationError(_)));
2459 if let RelayerError::ValidationError(msg) = err {
2460 assert!(msg.contains("Simulation failed"));
2461 }
2462 }
2463
2464 #[test]
2465 fn test_calculate_total_soroban_fee_minimum_fee() {
2466 let sim_response = soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2468 error: None,
2469 transaction_data: "".to_string(),
2470 min_resource_fee: 0, ..Default::default()
2472 };
2473
2474 let result = calculate_total_soroban_fee(&sim_response, 1);
2475 assert!(result.is_ok());
2476 let fee = result.unwrap();
2478 assert!(fee >= STELLAR_DEFAULT_TRANSACTION_FEE);
2479 }
2480
2481 #[test]
2486 fn test_build_soroban_transaction_envelope_success() {
2487 use soroban_rs::xdr::{
2488 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Operation,
2489 OperationBody, ScAddress, ScSymbol, VecM,
2490 };
2491
2492 let contract_id = ContractId(Hash([1u8; 32]));
2493 let invoke_args = InvokeContractArgs {
2494 contract_address: ScAddress::Contract(contract_id),
2495 function_name: ScSymbol("test".try_into().unwrap()),
2496 args: VecM::default(),
2497 };
2498
2499 let invoke_op = InvokeHostFunctionOp {
2500 host_function: HostFunction::InvokeContract(invoke_args),
2501 auth: VecM::default(),
2502 };
2503
2504 let operation = Operation {
2505 source_account: None,
2506 body: OperationBody::InvokeHostFunction(invoke_op),
2507 };
2508
2509 let result = build_soroban_transaction_envelope(TEST_PK, operation.clone(), 100);
2510 assert!(result.is_ok());
2511
2512 let envelope = result.unwrap();
2513 if let TransactionEnvelope::Tx(tx_env) = envelope {
2514 assert_eq!(tx_env.tx.fee, 100);
2515 assert_eq!(tx_env.tx.seq_num.0, 0); assert_eq!(tx_env.tx.operations.len(), 1);
2517 } else {
2518 panic!("Expected Tx envelope");
2519 }
2520 }
2521
2522 #[test]
2523 fn test_build_soroban_transaction_envelope_invalid_source() {
2524 use soroban_rs::xdr::{
2525 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Operation,
2526 OperationBody, ScAddress, ScSymbol, VecM,
2527 };
2528
2529 let contract_id = ContractId(Hash([1u8; 32]));
2530 let invoke_args = InvokeContractArgs {
2531 contract_address: ScAddress::Contract(contract_id),
2532 function_name: ScSymbol("test".try_into().unwrap()),
2533 args: VecM::default(),
2534 };
2535
2536 let invoke_op = InvokeHostFunctionOp {
2537 host_function: HostFunction::InvokeContract(invoke_args),
2538 auth: VecM::default(),
2539 };
2540
2541 let operation = Operation {
2542 source_account: None,
2543 body: OperationBody::InvokeHostFunction(invoke_op),
2544 };
2545
2546 let result = build_soroban_transaction_envelope("INVALID_ADDRESS", operation, 100);
2547 assert!(result.is_err());
2548 assert!(matches!(
2549 result.unwrap_err(),
2550 RelayerError::ValidationError(_)
2551 ));
2552 }
2553
2554 #[test]
2559 fn test_add_payment_operation_to_envelope_classic() {
2560 let envelope = create_test_envelope_for_payment();
2561 let fee_quote = FeeQuote {
2562 fee_in_token: 1000000,
2563 fee_in_token_ui: "1.0".to_string(),
2564 fee_in_stroops: 10000,
2565 conversion_rate: 100.0,
2566 };
2567
2568 let result = add_payment_operation_to_envelope(envelope, &fee_quote, USDC_ASSET, TEST_PK);
2569 assert!(result.is_ok());
2570
2571 let updated_envelope = result.unwrap();
2572 if let TransactionEnvelope::Tx(tx_env) = updated_envelope {
2574 assert_eq!(tx_env.tx.operations.len(), 2);
2575 }
2576 }
2577
2578 #[test]
2579 fn test_add_payment_operation_to_envelope_soroban_no_op_added() {
2580 use soroban_rs::xdr::{
2581 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo,
2582 Operation, OperationBody, Preconditions, ScAddress, ScSymbol, SequenceNumber,
2583 Transaction, TransactionEnvelope, TransactionExt, TransactionV1Envelope, Uint256, VecM,
2584 };
2585
2586 let source_pk = Ed25519PublicKey::from_string(
2588 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2589 )
2590 .unwrap();
2591
2592 let contract_id = ContractId(Hash([1u8; 32]));
2593 let invoke_args = InvokeContractArgs {
2594 contract_address: ScAddress::Contract(contract_id),
2595 function_name: ScSymbol("test".try_into().unwrap()),
2596 args: VecM::default(),
2597 };
2598
2599 let invoke_op = InvokeHostFunctionOp {
2600 host_function: HostFunction::InvokeContract(invoke_args),
2601 auth: VecM::default(),
2602 };
2603
2604 let tx = Transaction {
2605 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2606 fee: 100,
2607 seq_num: SequenceNumber(1),
2608 cond: Preconditions::None,
2609 memo: Memo::None,
2610 operations: vec![Operation {
2611 source_account: None,
2612 body: OperationBody::InvokeHostFunction(invoke_op),
2613 }]
2614 .try_into()
2615 .unwrap(),
2616 ext: TransactionExt::V0,
2617 };
2618
2619 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2620 tx,
2621 signatures: VecM::default(),
2622 });
2623
2624 let fee_quote = FeeQuote {
2625 fee_in_token: 1000000,
2626 fee_in_token_ui: "1.0".to_string(),
2627 fee_in_stroops: 10000,
2628 conversion_rate: 100.0,
2629 };
2630
2631 let result = add_payment_operation_to_envelope(envelope, &fee_quote, USDC_ASSET, TEST_PK);
2632 assert!(result.is_ok());
2633
2634 let updated_envelope = result.unwrap();
2636 if let TransactionEnvelope::Tx(tx_env) = updated_envelope {
2637 assert_eq!(tx_env.tx.operations.len(), 1); }
2639 }
2640
2641 fn create_test_envelope_for_payment() -> TransactionEnvelope {
2643 let source_pk = Ed25519PublicKey::from_string(
2644 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2645 )
2646 .unwrap();
2647 let dest_pk = Ed25519PublicKey::from_string(
2648 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
2649 )
2650 .unwrap();
2651
2652 let payment_op = PaymentOp {
2653 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
2654 asset: soroban_rs::xdr::Asset::Native,
2655 amount: 1000000,
2656 };
2657
2658 let tx = Transaction {
2659 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2660 fee: 100,
2661 seq_num: SequenceNumber(1),
2662 cond: Preconditions::None,
2663 memo: soroban_rs::xdr::Memo::None,
2664 operations: vec![Operation {
2665 source_account: None,
2666 body: OperationBody::Payment(payment_op),
2667 }]
2668 .try_into()
2669 .unwrap(),
2670 ext: TransactionExt::V0,
2671 };
2672
2673 TransactionEnvelope::Tx(TransactionV1Envelope {
2674 tx,
2675 signatures: VecM::default(),
2676 })
2677 }
2678
2679 #[test]
2684 fn test_add_fee_payment_operation_success() {
2685 let mut envelope = create_test_envelope_for_payment();
2686 let result = add_fee_payment_operation(&mut envelope, USDC_ASSET, 1000000, TEST_PK);
2687 assert!(result.is_ok());
2688
2689 if let TransactionEnvelope::Tx(tx_env) = envelope {
2691 assert_eq!(tx_env.tx.operations.len(), 2);
2692 }
2693 }
2694
2695 #[test]
2696 fn test_add_fee_payment_operation_native_asset() {
2697 let mut envelope = create_test_envelope_for_payment();
2698 let result = add_fee_payment_operation(&mut envelope, "native", 1000000, TEST_PK);
2699 assert!(result.is_ok());
2700 }
2701
2702 #[test]
2707 fn test_soroban_invoke_info_debug_clone() {
2708 use soroban_rs::xdr::ScVal;
2709
2710 let info = SorobanInvokeInfo {
2711 target_contract: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
2712 target_fn: "transfer".to_string(),
2713 target_args: vec![ScVal::Bool(true)],
2714 };
2715
2716 let debug_str = format!("{:?}", info);
2718 assert!(debug_str.contains("SorobanInvokeInfo"));
2719 assert!(debug_str.contains("transfer"));
2720
2721 let cloned = info.clone();
2723 assert_eq!(cloned.target_contract, info.target_contract);
2724 assert_eq!(cloned.target_fn, info.target_fn);
2725 assert_eq!(cloned.target_args.len(), info.target_args.len());
2726 }
2727
2728 #[tokio::test]
2733 async fn test_build_sponsored_transaction_non_user_fee_strategy() {
2734 let mut policy = RelayerStellarPolicy::default();
2736 policy.fee_payment_strategy = Some(crate::models::StellarFeePaymentStrategy::Relayer);
2737 policy.allowed_tokens = Some(vec![crate::models::StellarAllowedTokensPolicy {
2738 asset: USDC_ASSET.to_string(),
2739 metadata: None,
2740 max_allowed_fee: None,
2741 swap_config: None,
2742 }]);
2743
2744 let relayer_model = RelayerRepoModel {
2745 id: "test-relayer-id".to_string(),
2746 name: "Test Relayer".to_string(),
2747 network: "testnet".to_string(),
2748 paused: false,
2749 network_type: NetworkType::Stellar,
2750 signer_id: "signer-id".to_string(),
2751 policies: RelayerNetworkPolicy::Stellar(policy),
2752 address: TEST_PK.to_string(),
2753 notification_id: Some("notification-id".to_string()),
2754 system_disabled: false,
2755 custom_rpc_urls: None,
2756 ..Default::default()
2757 };
2758
2759 let provider = MockStellarProviderTrait::new();
2760 let dex_service = create_mock_dex_service();
2761 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
2762
2763 let transaction_xdr = create_test_transaction_xdr();
2764 let request = SponsoredTransactionBuildRequest::Stellar(
2765 crate::models::StellarPrepareTransactionRequestParams {
2766 transaction_xdr: Some(transaction_xdr),
2767 operations: None,
2768 source_account: None,
2769 fee_token: USDC_ASSET.to_string(),
2770 },
2771 );
2772
2773 let result = relayer.build_sponsored_transaction(request).await;
2774 assert!(result.is_err());
2775 let err = result.unwrap_err();
2776 assert!(matches!(err, RelayerError::ValidationError(_)));
2777 if let RelayerError::ValidationError(msg) = err {
2778 assert!(msg.contains("fee_payment_strategy: User"));
2779 }
2780 }
2781
2782 fn create_valid_soroban_transaction_data_xdr() -> String {
2788 use soroban_rs::xdr::{
2789 LedgerFootprint, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
2790 };
2791
2792 let soroban_data = SorobanTransactionData {
2793 ext: SorobanTransactionDataExt::V0,
2794 resources: SorobanResources {
2795 footprint: LedgerFootprint {
2796 read_only: VecM::default(),
2797 read_write: VecM::default(),
2798 },
2799 instructions: 1000000,
2800 disk_read_bytes: 10000,
2801 write_bytes: 1000,
2802 },
2803 resource_fee: 50000,
2804 };
2805
2806 soroban_data.to_xdr_base64(Limits::none()).unwrap()
2807 }
2808
2809 fn create_test_soroban_transaction_xdr() -> String {
2811 use soroban_rs::xdr::{
2812 ContractId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo,
2813 ScAddress, ScSymbol, ScVal,
2814 };
2815
2816 let source_pk = Ed25519PublicKey::from_string(
2817 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
2818 )
2819 .unwrap();
2820
2821 let contract_id = ContractId(Hash([1u8; 32]));
2823 let invoke_args = InvokeContractArgs {
2824 contract_address: ScAddress::Contract(contract_id),
2825 function_name: ScSymbol("transfer".try_into().unwrap()),
2826 args: vec![ScVal::Bool(true)].try_into().unwrap(),
2827 };
2828
2829 let invoke_op = InvokeHostFunctionOp {
2830 host_function: HostFunction::InvokeContract(invoke_args),
2831 auth: VecM::default(),
2832 };
2833
2834 let operation = Operation {
2835 source_account: None,
2836 body: OperationBody::InvokeHostFunction(invoke_op),
2837 };
2838
2839 let tx = Transaction {
2840 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
2841 fee: 100,
2842 seq_num: SequenceNumber(1),
2843 cond: Preconditions::None,
2844 memo: Memo::None,
2845 operations: vec![operation].try_into().unwrap(),
2846 ext: TransactionExt::V0,
2847 };
2848
2849 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2850 tx,
2851 signatures: VecM::default(),
2852 });
2853
2854 envelope.to_xdr_base64(Limits::none()).unwrap()
2855 }
2856
2857 fn create_test_relayer_with_soroban_token() -> RelayerRepoModel {
2859 let mut policy = RelayerStellarPolicy::default();
2860 policy.fee_payment_strategy = Some(crate::models::StellarFeePaymentStrategy::User);
2861 policy.allowed_tokens = Some(vec![crate::models::StellarAllowedTokensPolicy {
2863 asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
2864 metadata: None,
2865 max_allowed_fee: None,
2866 swap_config: None,
2867 }]);
2868
2869 RelayerRepoModel {
2870 id: "test-relayer-id".to_string(),
2871 name: "Test Relayer".to_string(),
2872 network: "testnet".to_string(),
2873 paused: false,
2874 network_type: NetworkType::Stellar,
2875 signer_id: "signer-id".to_string(),
2876 policies: RelayerNetworkPolicy::Stellar(policy),
2877 address: TEST_PK.to_string(),
2878 notification_id: Some("notification-id".to_string()),
2879 system_disabled: false,
2880 custom_rpc_urls: None,
2881 ..Default::default()
2882 }
2883 }
2884
2885 #[tokio::test]
2886 #[serial]
2887 async fn test_quote_soroban_from_xdr_success() {
2888 std::env::set_var(
2890 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
2891 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
2892 );
2893
2894 let relayer_model = create_test_relayer_with_soroban_token();
2895 let mut provider = MockStellarProviderTrait::new();
2896
2897 provider.expect_get_latest_ledger().returning(|| {
2899 Box::pin(ready(Ok(
2900 soroban_rs::stellar_rpc_client::GetLatestLedgerResponse {
2901 id: "test".to_string(),
2902 protocol_version: 20,
2903 sequence: 1000,
2904 },
2905 )))
2906 });
2907
2908 provider
2910 .expect_simulate_transaction_envelope()
2911 .returning(|_| {
2912 Box::pin(ready(Ok(
2913 soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
2914 min_resource_fee: 50000,
2915 transaction_data: "AAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAABgAAAAEAAAAGAAAAAG0JZTO9fU6p3NeJp5w3TpKhZmx6p1pR7mq9wFwCnEIuAAAAFAAAAAEAAAAAAAAAB8NVb2IAAAH0AAAAAQAAAAAAABfAAAAAAAAAAPUAAAAAAAAENgAAAAA=".to_string(),
2916 ..Default::default()
2917 },
2918 )))
2919 });
2920
2921 provider.expect_call_contract().returning(|_, _, _| {
2923 use soroban_rs::xdr::Int128Parts;
2924 Box::pin(ready(Ok(ScVal::I128(Int128Parts {
2926 hi: 0,
2927 lo: 10_000_000,
2928 }))))
2929 });
2930
2931 let mut dex_service = MockStellarDexServiceTrait::new();
2932 dex_service.expect_supported_asset_types().returning(|| {
2933 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
2934 });
2935
2936 dex_service
2938 .expect_get_xlm_to_token_quote()
2939 .returning(|_, _, _, _| {
2940 Box::pin(ready(Ok(
2941 crate::services::stellar_dex::StellarQuoteResponse {
2942 input_asset: "native".to_string(),
2943 output_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
2944 .to_string(),
2945 in_amount: 50100, out_amount: 1500000, price_impact_pct: 0.0,
2948 slippage_bps: 100,
2949 path: None,
2950 },
2951 )))
2952 });
2953
2954 let dex_service = Arc::new(dex_service);
2955 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
2956
2957 let transaction_xdr = create_test_soroban_transaction_xdr();
2958 let request = SponsoredTransactionQuoteRequest::Stellar(
2959 crate::models::StellarFeeEstimateRequestParams {
2960 transaction_xdr: Some(transaction_xdr),
2961 operations: None,
2962 source_account: None,
2963 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
2964 },
2965 );
2966
2967 let result = relayer.quote_sponsored_transaction(request).await;
2968 if let Err(e) = &result {
2969 eprintln!("Soroban quote error: {:?}", e);
2970 }
2971 assert!(result.is_ok());
2972
2973 if let SponsoredTransactionQuoteResponse::Stellar(quote) = result.unwrap() {
2974 assert_eq!(quote.fee_in_token, "1500000");
2975 assert!(!quote.fee_in_token_ui.is_empty());
2976 assert!(!quote.conversion_rate.is_empty());
2977 } else {
2978 panic!("Expected Stellar quote response");
2979 }
2980
2981 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
2983 }
2984
2985 #[tokio::test]
2986 #[serial]
2987 async fn test_quote_soroban_from_xdr_missing_fee_forwarder() {
2988 std::env::remove_var("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS");
2990
2991 let mut relayer_model = create_test_relayer_with_soroban_token();
2993 relayer_model.network = "mainnet".to_string();
2994
2995 let provider = MockStellarProviderTrait::new();
2996
2997 let mut dex_service = MockStellarDexServiceTrait::new();
2998 dex_service.expect_supported_asset_types().returning(|| {
2999 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
3000 });
3001
3002 let dex_service = Arc::new(dex_service);
3003 let relayer = create_test_relayer_instance_with_network(
3004 relayer_model,
3005 provider,
3006 dex_service,
3007 create_test_mainnet_network(),
3008 )
3009 .await;
3010
3011 let transaction_xdr = create_test_soroban_transaction_xdr();
3012 let request = SponsoredTransactionQuoteRequest::Stellar(
3013 crate::models::StellarFeeEstimateRequestParams {
3014 transaction_xdr: Some(transaction_xdr),
3015 operations: None,
3016 source_account: None,
3017 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3018 },
3019 );
3020
3021 let result = relayer.quote_sponsored_transaction(request).await;
3022 assert!(result.is_err());
3023 let err = result.unwrap_err();
3024 assert!(matches!(err, RelayerError::ValidationError(_)));
3025 if let RelayerError::ValidationError(msg) = err {
3026 assert!(msg.contains("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS"));
3027 }
3028 }
3029
3030 #[tokio::test]
3031 #[serial]
3032 async fn test_quote_soroban_from_xdr_invalid_fee_token_format() {
3033 std::env::set_var(
3035 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
3036 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
3037 );
3038
3039 let mut policy = RelayerStellarPolicy::default();
3041 policy.fee_payment_strategy = Some(crate::models::StellarFeePaymentStrategy::User);
3042 policy.allowed_tokens = Some(vec![
3043 crate::models::StellarAllowedTokensPolicy {
3044 asset: USDC_ASSET.to_string(), metadata: None,
3046 max_allowed_fee: None,
3047 swap_config: None,
3048 },
3049 crate::models::StellarAllowedTokensPolicy {
3050 asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3051 metadata: None,
3052 max_allowed_fee: None,
3053 swap_config: None,
3054 },
3055 ]);
3056
3057 let relayer_model = RelayerRepoModel {
3058 id: "test-relayer-id".to_string(),
3059 name: "Test Relayer".to_string(),
3060 network: "testnet".to_string(),
3061 paused: false,
3062 network_type: NetworkType::Stellar,
3063 signer_id: "signer-id".to_string(),
3064 policies: RelayerNetworkPolicy::Stellar(policy),
3065 address: TEST_PK.to_string(),
3066 notification_id: Some("notification-id".to_string()),
3067 system_disabled: false,
3068 custom_rpc_urls: None,
3069 ..Default::default()
3070 };
3071
3072 let provider = MockStellarProviderTrait::new();
3073
3074 let mut dex_service = MockStellarDexServiceTrait::new();
3075 dex_service
3076 .expect_supported_asset_types()
3077 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
3078
3079 let dex_service = Arc::new(dex_service);
3080 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
3081
3082 let transaction_xdr = create_test_soroban_transaction_xdr();
3084 let request = SponsoredTransactionQuoteRequest::Stellar(
3085 crate::models::StellarFeeEstimateRequestParams {
3086 transaction_xdr: Some(transaction_xdr),
3087 operations: None,
3088 source_account: None,
3089 fee_token: USDC_ASSET.to_string(), },
3091 );
3092
3093 let result = relayer.quote_sponsored_transaction(request).await;
3094 assert!(result.is_err());
3095 let err = result.unwrap_err();
3096 assert!(matches!(err, RelayerError::ValidationError(_)));
3097 if let RelayerError::ValidationError(msg) = err {
3098 assert!(msg.contains("Soroban contract address"));
3099 }
3100
3101 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
3103 }
3104
3105 #[tokio::test]
3110 #[serial]
3111 async fn test_build_soroban_sponsored_success() {
3112 std::env::set_var(
3114 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
3115 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
3116 );
3117
3118 let relayer_model = create_test_relayer_with_soroban_token();
3119 let mut provider = MockStellarProviderTrait::new();
3120
3121 provider.expect_get_latest_ledger().returning(|| {
3123 Box::pin(ready(Ok(
3124 soroban_rs::stellar_rpc_client::GetLatestLedgerResponse {
3125 id: "test".to_string(),
3126 protocol_version: 20,
3127 sequence: 1000,
3128 },
3129 )))
3130 });
3131
3132 let valid_tx_data = create_valid_soroban_transaction_data_xdr();
3134 provider
3135 .expect_simulate_transaction_envelope()
3136 .returning(move |_| {
3137 let tx_data = valid_tx_data.clone();
3138 Box::pin(ready(Ok(
3139 soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
3140 min_resource_fee: 50000,
3141 transaction_data: tx_data,
3142 ..Default::default()
3143 },
3144 )))
3145 });
3146
3147 provider.expect_call_contract().returning(|_, _, _| {
3149 use soroban_rs::xdr::Int128Parts;
3150 Box::pin(ready(Ok(ScVal::I128(Int128Parts {
3152 hi: 0,
3153 lo: 10_000_000,
3154 }))))
3155 });
3156
3157 let mut dex_service = MockStellarDexServiceTrait::new();
3158 dex_service.expect_supported_asset_types().returning(|| {
3159 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
3160 });
3161
3162 dex_service
3164 .expect_get_xlm_to_token_quote()
3165 .returning(|_, _, _, _| {
3166 Box::pin(ready(Ok(
3167 crate::services::stellar_dex::StellarQuoteResponse {
3168 input_asset: "native".to_string(),
3169 output_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
3170 .to_string(),
3171 in_amount: 50100,
3172 out_amount: 1500000,
3173 price_impact_pct: 0.0,
3174 slippage_bps: 100,
3175 path: None,
3176 },
3177 )))
3178 });
3179
3180 let dex_service = Arc::new(dex_service);
3181 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
3182
3183 let transaction_xdr = create_test_soroban_transaction_xdr();
3184 let request = SponsoredTransactionBuildRequest::Stellar(
3185 crate::models::StellarPrepareTransactionRequestParams {
3186 transaction_xdr: Some(transaction_xdr),
3187 operations: None,
3188 source_account: None,
3189 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3190 },
3191 );
3192
3193 let result = relayer.build_sponsored_transaction(request).await;
3194 if let Err(e) = &result {
3195 eprintln!("Soroban build error: {:?}", e);
3196 }
3197 assert!(result.is_ok());
3198
3199 if let SponsoredTransactionBuildResponse::Stellar(build) = result.unwrap() {
3200 assert!(!build.transaction.is_empty());
3201 assert_eq!(build.fee_in_token, "1500000");
3202 assert!(!build.fee_in_token_ui.is_empty());
3203 assert_eq!(
3204 build.fee_token,
3205 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
3206 );
3207 assert!(!build.valid_until.is_empty());
3208 assert!(build.user_auth_entry.is_some());
3210 assert!(!build.user_auth_entry.unwrap().is_empty());
3211 } else {
3212 panic!("Expected Stellar build response");
3213 }
3214
3215 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
3217 }
3218
3219 #[tokio::test]
3220 #[serial]
3221 async fn test_build_soroban_sponsored_missing_fee_forwarder() {
3222 std::env::remove_var("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS");
3224
3225 let mut relayer_model = create_test_relayer_with_soroban_token();
3227 relayer_model.network = "mainnet".to_string();
3228
3229 let provider = MockStellarProviderTrait::new();
3230
3231 let mut dex_service = MockStellarDexServiceTrait::new();
3232 dex_service.expect_supported_asset_types().returning(|| {
3233 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
3234 });
3235
3236 let dex_service = Arc::new(dex_service);
3237 let relayer = create_test_relayer_instance_with_network(
3238 relayer_model,
3239 provider,
3240 dex_service,
3241 create_test_mainnet_network(),
3242 )
3243 .await;
3244
3245 let transaction_xdr = create_test_soroban_transaction_xdr();
3246 let request = SponsoredTransactionBuildRequest::Stellar(
3247 crate::models::StellarPrepareTransactionRequestParams {
3248 transaction_xdr: Some(transaction_xdr),
3249 operations: None,
3250 source_account: None,
3251 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3252 },
3253 );
3254
3255 let result = relayer.build_sponsored_transaction(request).await;
3256 assert!(result.is_err());
3257 let err = result.unwrap_err();
3258 assert!(matches!(err, RelayerError::ValidationError(_)));
3259 if let RelayerError::ValidationError(msg) = err {
3260 assert!(msg.contains("STELLAR_MAINNET_FEE_FORWARDER_ADDRESS"));
3261 }
3262 }
3263
3264 #[tokio::test]
3265 #[serial]
3266 async fn test_build_soroban_sponsored_insufficient_balance() {
3267 std::env::set_var(
3269 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
3270 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
3271 );
3272
3273 let relayer_model = create_test_relayer_with_soroban_token();
3274 let mut provider = MockStellarProviderTrait::new();
3275
3276 provider.expect_get_latest_ledger().returning(|| {
3278 Box::pin(ready(Ok(
3279 soroban_rs::stellar_rpc_client::GetLatestLedgerResponse {
3280 id: "test".to_string(),
3281 protocol_version: 20,
3282 sequence: 1000,
3283 },
3284 )))
3285 });
3286
3287 provider
3289 .expect_simulate_transaction_envelope()
3290 .returning(|_| {
3291 Box::pin(ready(Ok(
3292 soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
3293 min_resource_fee: 50000,
3294 transaction_data: "AAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAABgAAAAEAAAAGAAAAAG0JZTO9fU6p3NeJp5w3TpKhZmx6p1pR7mq9wFwCnEIuAAAAFAAAAAEAAAAAAAAAB8NVb2IAAAH0AAAAAQAAAAAAABfAAAAAAAAAAPUAAAAAAAAENgAAAAA=".to_string(),
3295 ..Default::default()
3296 },
3297 )))
3298 });
3299
3300 provider.expect_call_contract().returning(|_, _, _| {
3302 use soroban_rs::xdr::Int128Parts;
3303 Box::pin(ready(Ok(ScVal::I128(Int128Parts { hi: 0, lo: 100 }))))
3305 });
3306
3307 let mut dex_service = MockStellarDexServiceTrait::new();
3308 dex_service.expect_supported_asset_types().returning(|| {
3309 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
3310 });
3311
3312 dex_service
3314 .expect_get_xlm_to_token_quote()
3315 .returning(|_, _, _, _| {
3316 Box::pin(ready(Ok(
3317 crate::services::stellar_dex::StellarQuoteResponse {
3318 input_asset: "native".to_string(),
3319 output_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
3320 .to_string(),
3321 in_amount: 50100,
3322 out_amount: 1500000, price_impact_pct: 0.0,
3324 slippage_bps: 100,
3325 path: None,
3326 },
3327 )))
3328 });
3329
3330 let dex_service = Arc::new(dex_service);
3331 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
3332
3333 let transaction_xdr = create_test_soroban_transaction_xdr();
3334 let request = SponsoredTransactionBuildRequest::Stellar(
3335 crate::models::StellarPrepareTransactionRequestParams {
3336 transaction_xdr: Some(transaction_xdr),
3337 operations: None,
3338 source_account: None,
3339 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3340 },
3341 );
3342
3343 let result = relayer.build_sponsored_transaction(request).await;
3344 assert!(result.is_err());
3345 let err = result.unwrap_err();
3346 assert!(matches!(err, RelayerError::ValidationError(_)));
3347 if let RelayerError::ValidationError(msg) = err {
3348 assert!(msg.contains("Insufficient balance"));
3349 }
3350
3351 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
3353 }
3354
3355 #[tokio::test]
3356 #[serial]
3357 async fn test_build_soroban_sponsored_simulation_error() {
3358 std::env::set_var(
3360 "STELLAR_TESTNET_FEE_FORWARDER_ADDRESS",
3361 "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
3362 );
3363
3364 let relayer_model = create_test_relayer_with_soroban_token();
3365 let mut provider = MockStellarProviderTrait::new();
3366
3367 provider.expect_get_latest_ledger().returning(|| {
3369 Box::pin(ready(Ok(
3370 soroban_rs::stellar_rpc_client::GetLatestLedgerResponse {
3371 id: "test".to_string(),
3372 protocol_version: 20,
3373 sequence: 1000,
3374 },
3375 )))
3376 });
3377
3378 provider
3380 .expect_simulate_transaction_envelope()
3381 .returning(|_| {
3382 Box::pin(ready(Ok(
3383 soroban_rs::stellar_rpc_client::SimulateTransactionResponse {
3384 error: Some(
3385 "Contract execution failed: insufficient resources".to_string(),
3386 ),
3387 min_resource_fee: 0,
3388 transaction_data: "".to_string(),
3389 ..Default::default()
3390 },
3391 )))
3392 });
3393
3394 let mut dex_service = MockStellarDexServiceTrait::new();
3395 dex_service.expect_supported_asset_types().returning(|| {
3396 std::collections::HashSet::from([AssetType::Native, AssetType::Contract])
3397 });
3398
3399 dex_service
3401 .expect_get_xlm_to_token_quote()
3402 .returning(|_, _, _, _| {
3403 Box::pin(ready(Ok(
3404 crate::services::stellar_dex::StellarQuoteResponse {
3405 input_asset: "native".to_string(),
3406 output_asset: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
3407 .to_string(),
3408 in_amount: 100,
3409 out_amount: 1500,
3410 price_impact_pct: 0.0,
3411 slippage_bps: 100,
3412 path: None,
3413 },
3414 )))
3415 });
3416
3417 let dex_service = Arc::new(dex_service);
3418 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
3419
3420 let transaction_xdr = create_test_soroban_transaction_xdr();
3421 let request = SponsoredTransactionBuildRequest::Stellar(
3422 crate::models::StellarPrepareTransactionRequestParams {
3423 transaction_xdr: Some(transaction_xdr),
3424 operations: None,
3425 source_account: None,
3426 fee_token: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC".to_string(),
3427 },
3428 );
3429
3430 let result = relayer.build_sponsored_transaction(request).await;
3431 assert!(result.is_err());
3432 let err = result.unwrap_err();
3433 assert!(matches!(err, RelayerError::ValidationError(_)));
3435 if let RelayerError::ValidationError(msg) = err {
3436 assert!(msg.contains("Simulation failed"));
3437 }
3438
3439 std::env::remove_var("STELLAR_TESTNET_FEE_FORWARDER_ADDRESS");
3441 }
3442}