1use async_trait::async_trait;
7use chrono::Utc;
8use soroban_rs::xdr::{Limits, Operation, TransactionEnvelope, WriteXdr};
9use tracing::debug;
10
11use crate::constants::{
12 get_stellar_sponsored_transaction_validity_duration, STELLAR_DEFAULT_TRANSACTION_FEE,
13};
14use crate::domain::relayer::{
15 stellar::xdr_utils::parse_transaction_xdr, GasAbstractionTrait, RelayerError, StellarRelayer,
16};
17use crate::domain::transaction::stellar::{
18 utils::{
19 add_operation_to_envelope, convert_xlm_fee_to_token, create_fee_payment_operation,
20 estimate_fee, set_time_bounds, FeeQuote,
21 },
22 StellarTransactionValidator,
23};
24use crate::domain::xdr_needs_simulation;
25use crate::jobs::JobProducerTrait;
26use crate::models::{
27 transaction::stellar::OperationSpec, SponsoredTransactionBuildRequest,
28 SponsoredTransactionBuildResponse, SponsoredTransactionQuoteRequest,
29 SponsoredTransactionQuoteResponse, StellarFeeEstimateResult, StellarPrepareTransactionResult,
30 StellarTransactionData, TransactionInput,
31};
32use crate::models::{NetworkRepoModel, RelayerRepoModel, TransactionRepoModel};
33use crate::repositories::{
34 NetworkRepository, RelayerRepository, Repository, TransactionRepository,
35};
36use crate::services::provider::StellarProviderTrait;
37use crate::services::signer::StellarSignTrait;
38use crate::services::stellar_dex::StellarDexServiceTrait;
39use crate::services::TransactionCounterServiceTrait;
40
41#[async_trait]
42impl<P, RR, NR, TR, J, TCS, S, D> GasAbstractionTrait
43 for StellarRelayer<P, RR, NR, TR, J, TCS, S, D>
44where
45 P: StellarProviderTrait + Send + Sync,
46 D: StellarDexServiceTrait + Send + Sync + 'static,
47 RR: Repository<RelayerRepoModel, String> + RelayerRepository + Send + Sync + 'static,
48 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
49 TR: Repository<TransactionRepoModel, String> + TransactionRepository + Send + Sync + 'static,
50 J: JobProducerTrait + Send + Sync + 'static,
51 TCS: TransactionCounterServiceTrait + Send + Sync + 'static,
52 S: StellarSignTrait + Send + Sync + 'static,
53{
54 async fn quote_sponsored_transaction(
55 &self,
56 params: SponsoredTransactionQuoteRequest,
57 ) -> Result<SponsoredTransactionQuoteResponse, RelayerError> {
58 let params = match params {
59 SponsoredTransactionQuoteRequest::Stellar(p) => p,
60 _ => {
61 return Err(RelayerError::ValidationError(
62 "Expected Stellar fee estimate request parameters".to_string(),
63 ));
64 }
65 };
66 debug!(
67 "Processing quote sponsored transaction request for token: {}",
68 params.fee_token
69 );
70
71 let policy = self.relayer.policies.get_stellar_policy();
73 StellarTransactionValidator::validate_allowed_token(¶ms.fee_token, &policy)
74 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
75
76 if params.transaction_xdr.is_none() && params.operations.is_none() {
78 return Err(RelayerError::ValidationError(
79 "Must provide either transaction_xdr or operations in the request".to_string(),
80 ));
81 }
82
83 let envelope = build_envelope_from_request(
85 params.transaction_xdr.as_ref(),
86 params.operations.as_ref(),
87 params.source_account.as_ref(),
88 &self.network.passphrase,
89 &self.provider,
90 )
91 .await?;
92
93 StellarTransactionValidator::gasless_transaction_validation(
95 &envelope,
96 &self.relayer.address,
97 &policy,
98 &self.provider,
99 None, )
101 .await
102 .map_err(|e| {
103 RelayerError::ValidationError(format!("Failed to validate gasless transaction: {e}"))
104 })?;
105
106 let inner_tx_fee = estimate_fee(&envelope, &self.provider, None)
108 .await
109 .map_err(crate::models::RelayerError::from)?;
110
111 let is_soroban = xdr_needs_simulation(&envelope).unwrap_or(false);
115 let mut additional_fees = 0;
116 if !is_soroban {
117 additional_fees = 2 * STELLAR_DEFAULT_TRANSACTION_FEE as u64; }
119 let xlm_fee = inner_tx_fee + additional_fees;
120
121 let fee_quote = convert_xlm_fee_to_token(
123 self.dex_service.as_ref(),
124 &policy,
125 xlm_fee,
126 ¶ms.fee_token,
127 )
128 .await
129 .map_err(crate::models::RelayerError::from)?;
130
131 StellarTransactionValidator::validate_max_fee(fee_quote.fee_in_stroops, &policy)
133 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
134
135 StellarTransactionValidator::validate_token_max_fee(
137 ¶ms.fee_token,
138 fee_quote.fee_in_token,
139 &policy,
140 )
141 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
142
143 StellarTransactionValidator::validate_user_token_balance(
145 &envelope,
146 ¶ms.fee_token,
147 fee_quote.fee_in_token,
148 &self.provider,
149 )
150 .await
151 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
152
153 debug!("Fee estimate result: {:?}", fee_quote);
154
155 let result = StellarFeeEstimateResult {
156 fee_in_token_ui: fee_quote.fee_in_token_ui,
157 fee_in_token: fee_quote.fee_in_token.to_string(),
158 conversion_rate: fee_quote.conversion_rate.to_string(),
159 };
160 Ok(SponsoredTransactionQuoteResponse::Stellar(result))
161 }
162
163 async fn build_sponsored_transaction(
164 &self,
165 params: SponsoredTransactionBuildRequest,
166 ) -> Result<SponsoredTransactionBuildResponse, RelayerError> {
167 let params = match params {
168 SponsoredTransactionBuildRequest::Stellar(p) => p,
169 _ => {
170 return Err(RelayerError::ValidationError(
171 "Expected Stellar prepare transaction request parameters".to_string(),
172 ));
173 }
174 };
175 debug!(
176 "Processing prepare transaction request for token: {}",
177 params.fee_token
178 );
179
180 let policy = self.relayer.policies.get_stellar_policy();
182 StellarTransactionValidator::validate_allowed_token(¶ms.fee_token, &policy)
183 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
184
185 if params.transaction_xdr.is_none() && params.operations.is_none() {
187 return Err(RelayerError::ValidationError(
188 "Must provide either transaction_xdr or operations in the request".to_string(),
189 ));
190 }
191
192 let envelope = build_envelope_from_request(
194 params.transaction_xdr.as_ref(),
195 params.operations.as_ref(),
196 params.source_account.as_ref(),
197 &self.network.passphrase,
198 &self.provider,
199 )
200 .await?;
201
202 StellarTransactionValidator::gasless_transaction_validation(
203 &envelope,
204 &self.relayer.address,
205 &policy,
206 &self.provider,
207 None, )
209 .await
210 .map_err(|e| {
211 RelayerError::ValidationError(format!("Failed to validate gasless transaction: {e}"))
212 })?;
213
214 let inner_tx_fee = estimate_fee(&envelope, &self.provider, None)
217 .await
218 .map_err(crate::models::RelayerError::from)?;
219
220 let is_soroban = xdr_needs_simulation(&envelope).unwrap_or(false);
224 let mut additional_fees = 0;
225 if !is_soroban {
226 additional_fees = 2 * STELLAR_DEFAULT_TRANSACTION_FEE as u64; }
228 let xlm_fee = inner_tx_fee + additional_fees;
229
230 debug!(
231 inner_tx_fee = inner_tx_fee,
232 additional_fees = additional_fees,
233 total_fee = xlm_fee,
234 "Fee estimated: inner transaction + fee payment op + fee-bump transaction fee"
235 );
236
237 let preliminary_fee_quote = convert_xlm_fee_to_token(
239 self.dex_service.as_ref(),
240 &policy,
241 xlm_fee,
242 ¶ms.fee_token,
243 )
244 .await
245 .map_err(crate::models::RelayerError::from)?;
246
247 StellarTransactionValidator::validate_max_fee(
249 preliminary_fee_quote.fee_in_stroops,
250 &policy,
251 )
252 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
253
254 StellarTransactionValidator::validate_token_max_fee(
256 ¶ms.fee_token,
257 preliminary_fee_quote.fee_in_token,
258 &policy,
259 )
260 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
261
262 StellarTransactionValidator::validate_user_token_balance(
264 &envelope,
265 ¶ms.fee_token,
266 preliminary_fee_quote.fee_in_token,
267 &self.provider,
268 )
269 .await
270 .map_err(|e| RelayerError::ValidationError(e.to_string()))?;
271
272 let mut final_envelope = add_payment_operation_to_envelope(
274 envelope,
275 &preliminary_fee_quote,
276 ¶ms.fee_token,
277 &self.relayer.address,
278 )?;
279
280 let fee_quote = preliminary_fee_quote;
282
283 debug!(
284 estimated_fee = xlm_fee,
285 final_fee_in_token = fee_quote.fee_in_token_ui,
286 "Transaction prepared successfully"
287 );
288
289 let valid_until = Utc::now() + get_stellar_sponsored_transaction_validity_duration();
291 set_time_bounds(&mut final_envelope, valid_until)
292 .map_err(crate::models::RelayerError::from)?;
293
294 let extended_xdr = final_envelope
296 .to_xdr_base64(Limits::none())
297 .map_err(|e| RelayerError::Internal(format!("Failed to serialize XDR: {e}")))?;
298
299 Ok(SponsoredTransactionBuildResponse::Stellar(
300 StellarPrepareTransactionResult {
301 transaction: extended_xdr,
302 fee_in_token: fee_quote.fee_in_token.to_string(),
303 fee_in_token_ui: fee_quote.fee_in_token_ui,
304 fee_in_stroops: fee_quote.fee_in_stroops.to_string(),
305 fee_token: params.fee_token,
306 valid_until: valid_until.to_rfc3339(),
307 },
308 ))
309 }
310}
311
312fn add_payment_operation_to_envelope(
330 mut envelope: TransactionEnvelope,
331 fee_quote: &FeeQuote,
332 fee_token: &str,
333 relayer_address: &str,
334) -> Result<TransactionEnvelope, RelayerError> {
335 let fee_amount = i64::try_from(fee_quote.fee_in_token).map_err(|_| {
337 RelayerError::Internal(
338 "Fee amount too large for payment operation (exceeds i64::MAX)".to_string(),
339 )
340 })?;
341
342 let is_soroban = xdr_needs_simulation(&envelope).unwrap_or(false);
343 if !is_soroban {
345 add_fee_payment_operation(&mut envelope, fee_token, fee_amount, relayer_address)?;
347 }
348
349 Ok(envelope)
350}
351
352async fn build_envelope_from_request<P>(
362 transaction_xdr: Option<&String>,
363 operations: Option<&Vec<OperationSpec>>,
364 source_account: Option<&String>,
365 network_passphrase: &str,
366 provider: &P,
367) -> Result<TransactionEnvelope, RelayerError>
368where
369 P: StellarProviderTrait + Send + Sync,
370{
371 if let Some(xdr) = transaction_xdr {
372 parse_transaction_xdr(xdr, false)
373 .map_err(|e| RelayerError::Internal(format!("Failed to parse XDR: {e}")))
374 } else if let Some(ops) = operations {
375 let source_account = source_account.ok_or_else(|| {
377 RelayerError::ValidationError(
378 "source_account is required when providing operations".to_string(),
379 )
380 })?;
381
382 let account_entry = provider.get_account(source_account).await.map_err(|e| {
386 RelayerError::Internal(format!(
387 "Failed to fetch account sequence number for {source_account}: {e}",
388 ))
389 })?;
390
391 let next_sequence = account_entry.seq_num.0 + 1;
393
394 let stellar_data = StellarTransactionData {
395 source_account: source_account.clone(),
396 fee: None,
397 sequence_number: Some(next_sequence as i64),
398 memo: None,
399 valid_until: None,
400 network_passphrase: network_passphrase.to_string(),
401 signatures: vec![],
402 hash: None,
403 simulation_transaction_data: None,
404 transaction_input: TransactionInput::Operations(ops.clone()),
405 signed_envelope_xdr: None,
406 };
407
408 stellar_data.build_unsigned_envelope().map_err(|e| {
410 RelayerError::Internal(format!("Failed to build envelope from operations: {e}"))
411 })
412 } else {
413 Err(RelayerError::ValidationError(
414 "Must provide either transaction_xdr or operations in the request".to_string(),
415 ))
416 }
417}
418
419fn add_fee_payment_operation(
421 envelope: &mut TransactionEnvelope,
422 fee_token: &str,
423 fee_amount: i64,
424 relayer_address: &str,
425) -> Result<(), RelayerError> {
426 let payment_op_spec = create_fee_payment_operation(relayer_address, fee_token, fee_amount)
427 .map_err(crate::models::RelayerError::from)?;
428
429 let payment_op = Operation::try_from(payment_op_spec)
431 .map_err(|e| RelayerError::Internal(format!("Failed to convert payment operation: {e}")))?;
432
433 add_operation_to_envelope(envelope, payment_op).map_err(crate::models::RelayerError::from)?;
435
436 Ok(())
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use crate::domain::transaction::stellar::utils::parse_account_id;
443 use crate::services::stellar_dex::AssetType;
444 use crate::{
445 config::{NetworkConfigCommon, StellarNetworkConfig},
446 jobs::MockJobProducerTrait,
447 models::{
448 transaction::stellar::OperationSpec, AssetSpec, NetworkConfigData, NetworkRepoModel,
449 NetworkType, RelayerNetworkPolicy, RelayerRepoModel, RelayerStellarPolicy,
450 SponsoredTransactionBuildRequest, SponsoredTransactionQuoteRequest,
451 },
452 repositories::{
453 InMemoryNetworkRepository, MockRelayerRepository, MockTransactionRepository,
454 },
455 services::{
456 provider::MockStellarProviderTrait, signer::MockStellarSignTrait,
457 stellar_dex::MockStellarDexServiceTrait, MockTransactionCounterServiceTrait,
458 },
459 };
460 use mockall::predicate::*;
461 use soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse;
462 use soroban_rs::stellar_rpc_client::LedgerEntryResult;
463 use soroban_rs::xdr::{
464 AccountEntry, AccountEntryExt, AccountId, AlphaNum4, AssetCode4, LedgerEntry,
465 LedgerEntryData, LedgerEntryExt, LedgerKey, Limits, MuxedAccount, Operation, OperationBody,
466 PaymentOp, Preconditions, PublicKey, SequenceNumber, String32, Thresholds, Transaction,
467 TransactionEnvelope, TransactionExt, TransactionV1Envelope, TrustLineEntry,
468 TrustLineEntryExt, Uint256, VecM, WriteXdr,
469 };
470 use std::future::ready;
471 use std::sync::Arc;
472 use stellar_strkey::ed25519::PublicKey as Ed25519PublicKey;
473
474 const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
475 const TEST_NETWORK_PASSPHRASE: &str = "Test SDF Network ; September 2015";
476 const USDC_ASSET: &str = "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
477
478 fn create_test_transaction_xdr() -> String {
480 let source_pk = Ed25519PublicKey::from_string(
482 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2",
483 )
484 .unwrap();
485 let dest_pk = Ed25519PublicKey::from_string(
486 "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ",
487 )
488 .unwrap();
489
490 let payment_op = PaymentOp {
491 destination: MuxedAccount::Ed25519(Uint256(dest_pk.0)),
492 asset: soroban_rs::xdr::Asset::Native,
493 amount: 1000000,
494 };
495
496 let operation = Operation {
497 source_account: None,
498 body: OperationBody::Payment(payment_op),
499 };
500
501 let operations: VecM<Operation, 100> = vec![operation].try_into().unwrap();
502
503 let tx = Transaction {
504 source_account: MuxedAccount::Ed25519(Uint256(source_pk.0)),
505 fee: 100,
506 seq_num: SequenceNumber(2), cond: Preconditions::None,
508 memo: soroban_rs::xdr::Memo::None,
509 operations,
510 ext: TransactionExt::V0,
511 };
512
513 let envelope = TransactionV1Envelope {
514 tx,
515 signatures: vec![].try_into().unwrap(),
516 };
517
518 let tx_envelope = TransactionEnvelope::Tx(envelope);
519 tx_envelope.to_xdr_base64(Limits::none()).unwrap()
520 }
521
522 fn create_test_relayer_with_user_fee_strategy() -> RelayerRepoModel {
524 let mut policy = RelayerStellarPolicy::default();
525 policy.fee_payment_strategy = Some(crate::models::StellarFeePaymentStrategy::User);
526 policy.allowed_tokens = Some(vec![crate::models::StellarAllowedTokensPolicy {
527 asset: USDC_ASSET.to_string(),
528 metadata: None,
529 max_allowed_fee: None,
530 swap_config: None,
531 }]);
532
533 RelayerRepoModel {
534 id: "test-relayer-id".to_string(),
535 name: "Test Relayer".to_string(),
536 network: "testnet".to_string(),
537 paused: false,
538 network_type: NetworkType::Stellar,
539 signer_id: "signer-id".to_string(),
540 policies: RelayerNetworkPolicy::Stellar(policy),
541 address: TEST_PK.to_string(),
542 notification_id: Some("notification-id".to_string()),
543 system_disabled: false,
544 custom_rpc_urls: None,
545 ..Default::default()
546 }
547 }
548
549 fn create_mock_dex_service() -> Arc<MockStellarDexServiceTrait> {
551 let mut mock_dex = MockStellarDexServiceTrait::new();
552 mock_dex
553 .expect_supported_asset_types()
554 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
555 Arc::new(mock_dex)
556 }
557
558 fn create_test_network() -> NetworkRepoModel {
560 NetworkRepoModel {
561 id: "stellar:testnet".to_string(),
562 name: "testnet".to_string(),
563 network_type: NetworkType::Stellar,
564 config: NetworkConfigData::Stellar(StellarNetworkConfig {
565 common: NetworkConfigCommon {
566 network: "testnet".to_string(),
567 from: None,
568 rpc_urls: Some(vec!["https://horizon-testnet.stellar.org".to_string()]),
569 explorer_urls: None,
570 average_blocktime_ms: Some(5000),
571 is_testnet: Some(true),
572 tags: None,
573 },
574 passphrase: Some(TEST_NETWORK_PASSPHRASE.to_string()),
575 horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
576 }),
577 }
578 }
579
580 async fn create_test_relayer_instance(
582 relayer_model: RelayerRepoModel,
583 provider: MockStellarProviderTrait,
584 dex_service: Arc<MockStellarDexServiceTrait>,
585 ) -> crate::domain::relayer::stellar::StellarRelayer<
586 MockStellarProviderTrait,
587 MockRelayerRepository,
588 InMemoryNetworkRepository,
589 MockTransactionRepository,
590 MockJobProducerTrait,
591 MockTransactionCounterServiceTrait,
592 MockStellarSignTrait,
593 MockStellarDexServiceTrait,
594 > {
595 let network_repository = Arc::new(InMemoryNetworkRepository::new());
596 let test_network = create_test_network();
597 network_repository.create(test_network).await.unwrap();
598
599 let relayer_repo = Arc::new(MockRelayerRepository::new());
600 let tx_repo = Arc::new(MockTransactionRepository::new());
601 let job_producer = Arc::new(MockJobProducerTrait::new());
602 let counter = Arc::new(MockTransactionCounterServiceTrait::new());
603 let signer = Arc::new(MockStellarSignTrait::new());
604
605 crate::domain::relayer::stellar::StellarRelayer::new(
606 relayer_model,
607 signer,
608 provider,
609 crate::domain::relayer::stellar::StellarRelayerDependencies::new(
610 relayer_repo,
611 network_repository,
612 tx_repo,
613 counter,
614 job_producer,
615 ),
616 dex_service,
617 )
618 .await
619 .unwrap()
620 }
621
622 #[tokio::test]
623 async fn test_quote_sponsored_transaction_with_xdr() {
624 let relayer_model = create_test_relayer_with_user_fee_strategy();
625 let mut provider = MockStellarProviderTrait::new();
626
627 provider.expect_get_account().returning(|_| {
629 Box::pin(ready(Ok(AccountEntry {
630 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
631 balance: 1000000000,
632 seq_num: SequenceNumber(1),
633 num_sub_entries: 0,
634 inflation_dest: None,
635 flags: 0,
636 home_domain: String32::default(),
637 thresholds: Thresholds([0; 4]),
638 signers: VecM::default(),
639 ext: AccountEntryExt::V0,
640 })))
641 });
642
643 provider.expect_get_ledger_entries().returning(|keys| {
646 let account_id = if let Some(LedgerKey::Trustline(trustline_key)) = keys.first() {
648 trustline_key.account_id.clone()
649 } else {
650 parse_account_id(TEST_PK).unwrap_or_else(|_| {
652 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
653 })
654 };
655
656 let issuer_id =
657 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
658 .unwrap_or_else(|_| {
659 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
660 });
661
662 let trustline_entry = TrustLineEntry {
664 account_id,
665 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
666 asset_code: AssetCode4(*b"USDC"),
667 issuer: issuer_id,
668 }),
669 balance: 10_000_000i64,
670 limit: i64::MAX,
671 flags: 0,
672 ext: TrustLineEntryExt::V0,
673 };
674
675 let ledger_entry = LedgerEntry {
676 last_modified_ledger_seq: 0,
677 data: LedgerEntryData::Trustline(trustline_entry),
678 ext: LedgerEntryExt::V0,
679 };
680
681 let xdr = ledger_entry
683 .data
684 .to_xdr_base64(soroban_rs::xdr::Limits::none())
685 .expect("Failed to encode trustline entry data to XDR");
686
687 Box::pin(ready(Ok(GetLedgerEntriesResponse {
688 entries: Some(vec![LedgerEntryResult {
689 key: "test_key".to_string(),
690 xdr,
691 last_modified_ledger: 0u32,
692 live_until_ledger_seq_ledger_seq: None,
693 }]),
694 latest_ledger: 0,
695 })))
696 });
697
698 let mut dex_service = MockStellarDexServiceTrait::new();
699 dex_service
700 .expect_supported_asset_types()
701 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
702
703 dex_service
705 .expect_get_xlm_to_token_quote()
706 .returning(|_, _, _, _| {
707 Box::pin(ready(Ok(
708 crate::services::stellar_dex::StellarQuoteResponse {
709 input_asset: "native".to_string(),
710 output_asset: USDC_ASSET.to_string(),
711 in_amount: 100000,
712 out_amount: 1500000,
713 price_impact_pct: 0.0,
714 slippage_bps: 100,
715 path: None,
716 },
717 )))
718 });
719
720 let dex_service = Arc::new(dex_service);
721 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
722
723 let transaction_xdr = create_test_transaction_xdr();
724 let request = SponsoredTransactionQuoteRequest::Stellar(
725 crate::models::StellarFeeEstimateRequestParams {
726 transaction_xdr: Some(transaction_xdr),
727 operations: None,
728 source_account: None,
729 fee_token: USDC_ASSET.to_string(),
730 },
731 );
732
733 let result = relayer.quote_sponsored_transaction(request).await;
734 if let Err(e) = &result {
735 eprintln!("Quote error: {:?}", e);
736 }
737 assert!(result.is_ok());
738
739 if let SponsoredTransactionQuoteResponse::Stellar(quote) = result.unwrap() {
740 assert_eq!(quote.fee_in_token, "1500000");
741 assert!(!quote.fee_in_token_ui.is_empty());
742 assert!(!quote.conversion_rate.is_empty());
743 } else {
744 panic!("Expected Stellar quote response");
745 }
746 }
747
748 #[tokio::test]
749 async fn test_quote_sponsored_transaction_with_operations() {
750 let relayer_model = create_test_relayer_with_user_fee_strategy();
751 let mut provider = MockStellarProviderTrait::new();
752
753 provider.expect_get_account().returning(|_| {
754 Box::pin(ready(Ok(AccountEntry {
755 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
756 balance: 1000000000,
757 seq_num: SequenceNumber(-1),
758 num_sub_entries: 0,
759 inflation_dest: None,
760 flags: 0,
761 home_domain: String32::default(),
762 thresholds: Thresholds([0; 4]),
763 signers: VecM::default(),
764 ext: AccountEntryExt::V0,
765 })))
766 });
767
768 provider.expect_get_ledger_entries().returning(|keys| {
771 let account_id = if let Some(LedgerKey::Trustline(trustline_key)) = keys.first() {
773 trustline_key.account_id.clone()
774 } else {
775 parse_account_id("GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2")
777 .unwrap_or_else(|_| {
778 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
779 })
780 };
781
782 let issuer_id =
783 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
784 .unwrap_or_else(|_| {
785 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
786 });
787
788 let trustline_entry = TrustLineEntry {
790 account_id,
791 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
792 asset_code: AssetCode4(*b"USDC"),
793 issuer: issuer_id,
794 }),
795 balance: 10_000_000i64,
796 limit: i64::MAX,
797 flags: 0,
798 ext: TrustLineEntryExt::V0,
799 };
800
801 let ledger_entry = LedgerEntry {
802 last_modified_ledger_seq: 0,
803 data: LedgerEntryData::Trustline(trustline_entry),
804 ext: LedgerEntryExt::V0,
805 };
806
807 let xdr = ledger_entry
809 .data
810 .to_xdr_base64(soroban_rs::xdr::Limits::none())
811 .expect("Failed to encode trustline entry data to XDR");
812
813 Box::pin(ready(Ok(
814 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
815 entries: Some(vec![LedgerEntryResult {
816 key: "test_key".to_string(),
817 xdr,
818 last_modified_ledger: 0u32,
819 live_until_ledger_seq_ledger_seq: None,
820 }]),
821 latest_ledger: 0,
822 },
823 )))
824 });
825
826 let mut dex_service = MockStellarDexServiceTrait::new();
827 dex_service
828 .expect_supported_asset_types()
829 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
830
831 dex_service
833 .expect_get_xlm_to_token_quote()
834 .returning(|_, _, _, _| {
835 Box::pin(ready(Ok(
836 crate::services::stellar_dex::StellarQuoteResponse {
837 input_asset: "native".to_string(),
838 output_asset: USDC_ASSET.to_string(),
839 in_amount: 100000,
840 out_amount: 1500000,
841 price_impact_pct: 0.0,
842 slippage_bps: 100,
843 path: None,
844 },
845 )))
846 });
847
848 let dex_service = Arc::new(dex_service);
849 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
850
851 let operations = vec![OperationSpec::Payment {
852 destination: TEST_PK.to_string(),
853 amount: 1000000,
854 asset: AssetSpec::Native,
855 }];
856
857 let request = SponsoredTransactionQuoteRequest::Stellar(
858 crate::models::StellarFeeEstimateRequestParams {
859 transaction_xdr: None,
860 operations: Some(operations),
861 source_account: Some(
862 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2".to_string(),
863 ),
864 fee_token: USDC_ASSET.to_string(),
865 },
866 );
867
868 let result = relayer.quote_sponsored_transaction(request).await;
869 if let Err(e) = &result {
870 eprintln!("Quote error: {:?}", e);
871 }
872 assert!(result.is_ok());
873 }
874
875 #[tokio::test]
876 async fn test_quote_sponsored_transaction_invalid_token() {
877 let relayer_model = create_test_relayer_with_user_fee_strategy();
878 let provider = MockStellarProviderTrait::new();
879 let dex_service = create_mock_dex_service();
880 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
881
882 let transaction_xdr = create_test_transaction_xdr();
883 let request = SponsoredTransactionQuoteRequest::Stellar(
884 crate::models::StellarFeeEstimateRequestParams {
885 transaction_xdr: Some(transaction_xdr),
886 operations: None,
887 source_account: None,
888 fee_token: "INVALID:TOKEN".to_string(),
889 },
890 );
891
892 let result = relayer.quote_sponsored_transaction(request).await;
893 assert!(result.is_err());
894 assert!(matches!(
895 result.unwrap_err(),
896 RelayerError::ValidationError(_)
897 ));
898 }
899
900 #[tokio::test]
901 async fn test_quote_sponsored_transaction_missing_xdr_and_operations() {
902 let relayer_model = create_test_relayer_with_user_fee_strategy();
903 let provider = MockStellarProviderTrait::new();
904 let dex_service = create_mock_dex_service();
905 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
906
907 let request = SponsoredTransactionQuoteRequest::Stellar(
908 crate::models::StellarFeeEstimateRequestParams {
909 transaction_xdr: None,
910 operations: None,
911 source_account: None,
912 fee_token: USDC_ASSET.to_string(),
913 },
914 );
915
916 let result = relayer.quote_sponsored_transaction(request).await;
917 assert!(result.is_err());
918 assert!(matches!(
919 result.unwrap_err(),
920 RelayerError::ValidationError(_)
921 ));
922 }
923
924 #[tokio::test]
925 async fn test_build_sponsored_transaction_with_xdr() {
926 let relayer_model = create_test_relayer_with_user_fee_strategy();
927 let mut provider = MockStellarProviderTrait::new();
928
929 provider.expect_get_account().returning(|_| {
930 Box::pin(ready(Ok(AccountEntry {
931 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
932 balance: 1000000000,
933 seq_num: SequenceNumber(-1),
934 num_sub_entries: 0,
935 inflation_dest: None,
936 flags: 0,
937 home_domain: String32::default(),
938 thresholds: Thresholds([0; 4]),
939 signers: VecM::default(),
940 ext: AccountEntryExt::V0,
941 })))
942 });
943
944 provider.expect_get_ledger_entries().returning(|keys| {
947 let account_id = if let Some(LedgerKey::Trustline(trustline_key)) = keys.first() {
949 trustline_key.account_id.clone()
950 } else {
951 parse_account_id(TEST_PK).unwrap_or_else(|_| {
953 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
954 })
955 };
956
957 let issuer_id =
958 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
959 .unwrap_or_else(|_| {
960 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
961 });
962
963 let trustline_entry = TrustLineEntry {
965 account_id,
966 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
967 asset_code: AssetCode4(*b"USDC"),
968 issuer: issuer_id,
969 }),
970 balance: 10_000_000i64, limit: i64::MAX,
972 flags: 0,
973 ext: TrustLineEntryExt::V0, };
975
976 let ledger_entry = LedgerEntry {
977 last_modified_ledger_seq: 0,
978 data: LedgerEntryData::Trustline(trustline_entry),
979 ext: LedgerEntryExt::V0,
980 };
981
982 let xdr = ledger_entry
984 .data
985 .to_xdr_base64(soroban_rs::xdr::Limits::none())
986 .expect("Failed to encode trustline entry data to XDR");
987
988 Box::pin(ready(Ok(
989 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
990 entries: Some(vec![LedgerEntryResult {
991 key: "test_key".to_string(),
992 xdr,
993 last_modified_ledger: 0u32,
994 live_until_ledger_seq_ledger_seq: None,
995 }]),
996 latest_ledger: 0,
997 },
998 )))
999 });
1000
1001 let mut dex_service = MockStellarDexServiceTrait::new();
1002 dex_service
1003 .expect_supported_asset_types()
1004 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1005
1006 dex_service
1008 .expect_get_xlm_to_token_quote()
1009 .returning(|_, _, _, _| {
1010 Box::pin(ready(Ok(
1011 crate::services::stellar_dex::StellarQuoteResponse {
1012 input_asset: "native".to_string(),
1013 output_asset: USDC_ASSET.to_string(),
1014 in_amount: 1000000,
1015 out_amount: 1500000,
1016 price_impact_pct: 0.0,
1017 slippage_bps: 100,
1018 path: None,
1019 },
1020 )))
1021 });
1022
1023 let dex_service = Arc::new(dex_service);
1024 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1025
1026 let transaction_xdr = create_test_transaction_xdr();
1027 let request = SponsoredTransactionBuildRequest::Stellar(
1028 crate::models::StellarPrepareTransactionRequestParams {
1029 transaction_xdr: Some(transaction_xdr),
1030 operations: None,
1031 source_account: None,
1032 fee_token: USDC_ASSET.to_string(),
1033 },
1034 );
1035
1036 let result = relayer.build_sponsored_transaction(request).await;
1037 assert!(result.is_ok());
1038
1039 if let SponsoredTransactionBuildResponse::Stellar(build) = result.unwrap() {
1040 assert!(!build.transaction.is_empty());
1041 assert_eq!(build.fee_in_token, "1500000");
1042 assert!(!build.fee_in_token_ui.is_empty());
1043 assert_eq!(build.fee_token, USDC_ASSET);
1044 assert!(!build.valid_until.is_empty());
1045 } else {
1046 panic!("Expected Stellar build response");
1047 }
1048 }
1049
1050 #[tokio::test]
1051 async fn test_build_sponsored_transaction_with_operations() {
1052 let relayer_model = create_test_relayer_with_user_fee_strategy();
1053 let mut provider = MockStellarProviderTrait::new();
1054
1055 provider.expect_get_account().returning(|_| {
1056 Box::pin(ready(Ok(AccountEntry {
1057 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1058 balance: 1000000000,
1059 seq_num: SequenceNumber(-1),
1060 num_sub_entries: 0,
1061 inflation_dest: None,
1062 flags: 0,
1063 home_domain: String32::default(),
1064 thresholds: Thresholds([0; 4]),
1065 signers: VecM::default(),
1066 ext: AccountEntryExt::V0,
1067 })))
1068 });
1069
1070 provider.expect_get_ledger_entries().returning(|_| {
1071 use crate::domain::transaction::stellar::utils::parse_account_id;
1072 use soroban_rs::stellar_rpc_client::LedgerEntryResult;
1073 use soroban_rs::xdr::{
1074 AccountId, AlphaNum4, AssetCode4, LedgerEntry, LedgerEntryData, LedgerEntryExt,
1075 PublicKey, TrustLineEntry, TrustLineEntryExt, Uint256, WriteXdr,
1076 };
1077
1078 let account_id =
1080 parse_account_id("GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2")
1081 .unwrap_or_else(|_| {
1082 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1083 });
1084 let issuer_id =
1085 parse_account_id("GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")
1086 .unwrap_or_else(|_| {
1087 AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])))
1088 });
1089
1090 let trustline_entry = TrustLineEntry {
1093 account_id,
1094 asset: soroban_rs::xdr::TrustLineAsset::CreditAlphanum4(AlphaNum4 {
1095 asset_code: AssetCode4(*b"USDC"),
1096 issuer: issuer_id,
1097 }),
1098 balance: 10_000_000i64,
1099 limit: i64::MAX,
1100 flags: 0,
1101 ext: TrustLineEntryExt::V0,
1102 };
1103
1104 let ledger_entry = LedgerEntry {
1105 last_modified_ledger_seq: 0,
1106 data: LedgerEntryData::Trustline(trustline_entry),
1107 ext: LedgerEntryExt::V0,
1108 };
1109
1110 let xdr = ledger_entry
1113 .data
1114 .to_xdr_base64(soroban_rs::xdr::Limits::none())
1115 .expect("Failed to encode trustline entry data to XDR");
1116
1117 Box::pin(ready(Ok(
1118 soroban_rs::stellar_rpc_client::GetLedgerEntriesResponse {
1119 entries: Some(vec![LedgerEntryResult {
1120 key: "test_key".to_string(),
1121 xdr,
1122 last_modified_ledger: 0u32,
1123 live_until_ledger_seq_ledger_seq: None,
1124 }]),
1125 latest_ledger: 0,
1126 },
1127 )))
1128 });
1129
1130 let mut dex_service = MockStellarDexServiceTrait::new();
1131 dex_service
1132 .expect_supported_asset_types()
1133 .returning(|| std::collections::HashSet::from([AssetType::Native, AssetType::Classic]));
1134
1135 dex_service
1136 .expect_get_xlm_to_token_quote()
1137 .returning(|_, _, _, _| {
1138 Box::pin(ready(Ok(
1139 crate::services::stellar_dex::StellarQuoteResponse {
1140 input_asset: "native".to_string(),
1141 output_asset: USDC_ASSET.to_string(),
1142 in_amount: 1000000,
1143 out_amount: 1500000,
1144 price_impact_pct: 0.0,
1145 slippage_bps: 100,
1146 path: None,
1147 },
1148 )))
1149 });
1150
1151 let dex_service = Arc::new(dex_service);
1152 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1153
1154 let operations = vec![OperationSpec::Payment {
1155 destination: TEST_PK.to_string(),
1156 amount: 1000000,
1157 asset: AssetSpec::Native,
1158 }];
1159
1160 let request = SponsoredTransactionBuildRequest::Stellar(
1161 crate::models::StellarPrepareTransactionRequestParams {
1162 transaction_xdr: None,
1163 operations: Some(operations),
1164 source_account: Some(
1165 "GCZ54QGQCUZ6U5WJF4AG5JEZCUMYTS2F6JRLUS76XF2PQMEJ2E3JISI2".to_string(),
1166 ),
1167 fee_token: USDC_ASSET.to_string(),
1168 },
1169 );
1170
1171 let result = relayer.build_sponsored_transaction(request).await;
1172
1173 assert!(result.is_ok());
1174 }
1175
1176 #[tokio::test]
1177 async fn test_build_sponsored_transaction_missing_source_account() {
1178 let relayer_model = create_test_relayer_with_user_fee_strategy();
1179 let provider = MockStellarProviderTrait::new();
1180 let dex_service = create_mock_dex_service();
1181 let relayer = create_test_relayer_instance(relayer_model, provider, dex_service).await;
1182
1183 let operations = vec![OperationSpec::Payment {
1184 destination: TEST_PK.to_string(),
1185 amount: 1000000,
1186 asset: AssetSpec::Native,
1187 }];
1188
1189 let request = SponsoredTransactionBuildRequest::Stellar(
1190 crate::models::StellarPrepareTransactionRequestParams {
1191 transaction_xdr: None,
1192 operations: Some(operations),
1193 source_account: None,
1194 fee_token: USDC_ASSET.to_string(),
1195 },
1196 );
1197
1198 let result = relayer.build_sponsored_transaction(request).await;
1199 assert!(result.is_err());
1200 assert!(matches!(
1201 result.unwrap_err(),
1202 RelayerError::ValidationError(_)
1203 ));
1204 }
1205
1206 #[tokio::test]
1207 async fn test_build_envelope_from_request_with_xdr() {
1208 let provider = MockStellarProviderTrait::new();
1209 let transaction_xdr = create_test_transaction_xdr();
1210 let result = build_envelope_from_request(
1211 Some(&transaction_xdr),
1212 None,
1213 None,
1214 TEST_NETWORK_PASSPHRASE,
1215 &provider,
1216 )
1217 .await;
1218 assert!(result.is_ok());
1219 }
1220
1221 #[tokio::test]
1222 async fn test_build_envelope_from_request_with_operations() {
1223 let mut provider = MockStellarProviderTrait::new();
1224
1225 provider.expect_get_account().returning(|_| {
1227 Box::pin(ready(Ok(AccountEntry {
1228 account_id: AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))),
1229 balance: 1000000000,
1230 seq_num: SequenceNumber(100),
1231 num_sub_entries: 0,
1232 inflation_dest: None,
1233 flags: 0,
1234 home_domain: String32::default(),
1235 thresholds: Thresholds([0; 4]),
1236 signers: VecM::default(),
1237 ext: AccountEntryExt::V0,
1238 })))
1239 });
1240
1241 let operations = vec![OperationSpec::Payment {
1242 destination: TEST_PK.to_string(),
1243 amount: 1000000,
1244 asset: AssetSpec::Native,
1245 }];
1246
1247 let result = build_envelope_from_request(
1248 None,
1249 Some(&operations),
1250 Some(&TEST_PK.to_string()),
1251 TEST_NETWORK_PASSPHRASE,
1252 &provider,
1253 )
1254 .await;
1255 assert!(result.is_ok());
1256
1257 if let Ok(envelope) = result {
1259 if let TransactionEnvelope::Tx(tx_env) = envelope {
1260 assert_eq!(tx_env.tx.seq_num.0, 101);
1261 }
1262 }
1263 }
1264
1265 #[tokio::test]
1266 async fn test_build_envelope_from_request_missing_source_account() {
1267 let provider = MockStellarProviderTrait::new();
1268 let operations = vec![OperationSpec::Payment {
1269 destination: TEST_PK.to_string(),
1270 amount: 1000000,
1271 asset: AssetSpec::Native,
1272 }];
1273
1274 let result = build_envelope_from_request(
1275 None,
1276 Some(&operations),
1277 None,
1278 TEST_NETWORK_PASSPHRASE,
1279 &provider,
1280 )
1281 .await;
1282 assert!(result.is_err());
1283 assert!(matches!(
1284 result.unwrap_err(),
1285 RelayerError::ValidationError(_)
1286 ));
1287 }
1288
1289 #[tokio::test]
1290 async fn test_build_envelope_from_request_missing_both() {
1291 let provider = MockStellarProviderTrait::new();
1292 let result =
1293 build_envelope_from_request(None, None, None, TEST_NETWORK_PASSPHRASE, &provider).await;
1294 assert!(result.is_err());
1295 assert!(matches!(
1296 result.unwrap_err(),
1297 RelayerError::ValidationError(_)
1298 ));
1299 }
1300
1301 #[tokio::test]
1302 async fn test_build_envelope_from_request_invalid_xdr() {
1303 let provider = MockStellarProviderTrait::new();
1304 let result = build_envelope_from_request(
1305 Some(&"INVALID_XDR".to_string()),
1306 None,
1307 None,
1308 TEST_NETWORK_PASSPHRASE,
1309 &provider,
1310 )
1311 .await;
1312 assert!(result.is_err());
1313 }
1314}