openzeppelin_relayer/domain/transaction/stellar/
validation.rs

1//! Validation logic for Stellar transactions
2//!
3//! This module focuses on business logic validations that aren't
4//! already handled by XDR parsing or the type system.
5
6use crate::constants::STELLAR_DEFAULT_TRANSACTION_FEE;
7use crate::constants::STELLAR_MAX_OPERATIONS;
8use crate::domain::relayer::xdr_utils::{
9    extract_operations, extract_source_account, muxed_account_to_string,
10};
11use crate::domain::transaction::stellar::token::get_token_balance;
12use crate::domain::transaction::stellar::utils::{
13    asset_to_asset_id, convert_xlm_fee_to_token, estimate_fee, extract_time_bounds,
14};
15use crate::domain::xdr_needs_simulation;
16use crate::models::RelayerStellarPolicy;
17use crate::models::{MemoSpec, OperationSpec, StellarValidationError, TransactionError};
18use crate::services::provider::StellarProviderTrait;
19use crate::services::stellar_dex::StellarDexServiceTrait;
20use chrono::{DateTime, Duration, Utc};
21use serde::Serialize;
22use soroban_rs::xdr::{
23    AccountId, HostFunction, InvokeHostFunctionOp, LedgerKey, OperationBody, PaymentOp,
24    PublicKey as XdrPublicKey, ScAddress, SorobanCredentials, TransactionEnvelope,
25};
26use stellar_strkey::ed25519::PublicKey;
27use thiserror::Error;
28#[derive(Debug, Error, Serialize)]
29pub enum StellarTransactionValidationError {
30    #[error("Validation error: {0}")]
31    ValidationError(String),
32    #[error("Policy violation: {0}")]
33    PolicyViolation(String),
34    #[error("Invalid asset identifier: {0}")]
35    InvalidAssetIdentifier(String),
36    #[error("Token not allowed: {0}")]
37    TokenNotAllowed(String),
38    #[error("Insufficient token payment: expected {0}, got {1}")]
39    InsufficientTokenPayment(u64, u64),
40    #[error("Max fee exceeded: {0}")]
41    MaxFeeExceeded(u64),
42}
43
44/// Validate operations for business rules
45pub fn validate_operations(ops: &[OperationSpec]) -> Result<(), TransactionError> {
46    // Basic sanity checks
47    if ops.is_empty() {
48        return Err(StellarValidationError::EmptyOperations.into());
49    }
50
51    if ops.len() > STELLAR_MAX_OPERATIONS {
52        return Err(StellarValidationError::TooManyOperations {
53            count: ops.len(),
54            max: STELLAR_MAX_OPERATIONS,
55        }
56        .into());
57    }
58
59    // Check Soroban exclusivity - this is a specific business rule
60    validate_soroban_exclusivity(ops)?;
61
62    Ok(())
63}
64
65/// Validate that Soroban operations are exclusive
66fn validate_soroban_exclusivity(ops: &[OperationSpec]) -> Result<(), TransactionError> {
67    let soroban_ops = ops.iter().filter(|op| is_soroban_operation(op)).count();
68
69    if soroban_ops > 1 {
70        return Err(StellarValidationError::MultipleSorobanOperations.into());
71    }
72
73    if soroban_ops == 1 && ops.len() > 1 {
74        return Err(StellarValidationError::SorobanNotExclusive.into());
75    }
76
77    Ok(())
78}
79
80/// Check if an operation is a Soroban operation
81fn is_soroban_operation(op: &OperationSpec) -> bool {
82    matches!(
83        op,
84        OperationSpec::InvokeContract { .. }
85            | OperationSpec::CreateContract { .. }
86            | OperationSpec::UploadWasm { .. }
87    )
88}
89
90/// Validate that Soroban operations don't have a non-None memo
91pub fn validate_soroban_memo_restriction(
92    ops: &[OperationSpec],
93    memo: &Option<MemoSpec>,
94) -> Result<(), TransactionError> {
95    let has_soroban = ops.iter().any(is_soroban_operation);
96
97    if has_soroban && memo.is_some() && !matches!(memo, Some(MemoSpec::None)) {
98        return Err(StellarValidationError::SorobanWithMemo.into());
99    }
100
101    Ok(())
102}
103
104/// Validator for Stellar transactions and policies
105pub struct StellarTransactionValidator;
106
107impl StellarTransactionValidator {
108    /// Validate fee_token structure
109    ///
110    /// Validates that the fee_token is in a valid format:
111    /// - "native" or "XLM" for native XLM
112    /// - "CODE:ISSUER" for classic assets (CODE: 1-12 chars, ISSUER: 56 chars starting with 'G')
113    /// - Contract address starting with "C" (56 chars) for Soroban contract tokens
114    pub fn validate_fee_token_structure(
115        fee_token: &str,
116    ) -> Result<(), StellarTransactionValidationError> {
117        // Handle native XLM
118        if fee_token == "native" || fee_token == "XLM" || fee_token.is_empty() {
119            return Ok(());
120        }
121
122        // Check if it's a contract address (starts with 'C', 56 chars)
123        if fee_token.starts_with('C') && fee_token.len() == 56 && !fee_token.contains(':') {
124            // Validate it's a valid contract address using StrKey
125            if stellar_strkey::Contract::from_string(fee_token).is_ok() {
126                return Ok(());
127            }
128            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
129                format!(
130                    "Invalid contract address format: {fee_token} (must be 56 characters and valid StrKey)"
131                ),
132            ));
133        }
134
135        // Otherwise, must be CODE:ISSUER format
136        let parts: Vec<&str> = fee_token.split(':').collect();
137        if parts.len() != 2 {
138            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(format!(
139                "Invalid fee_token format: {fee_token}. Expected 'native', 'CODE:ISSUER', or contract address (C...)"
140            )));
141        }
142
143        let code = parts[0];
144        let issuer = parts[1];
145
146        // Validate CODE length (1-12 characters)
147        if code.is_empty() || code.len() > 12 {
148            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
149                format!("Invalid asset code length: {code} (must be 1-12 characters)"),
150            ));
151        }
152
153        // Validate ISSUER format (56 chars, starts with 'G')
154        if issuer.len() != 56 {
155            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
156                format!("Invalid issuer address length: {issuer} (must be 56 characters)"),
157            ));
158        }
159
160        if !issuer.starts_with('G') {
161            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
162                format!("Invalid issuer address prefix: {issuer} (must start with 'G')"),
163            ));
164        }
165
166        // Validate issuer is a valid Stellar public key
167        if stellar_strkey::ed25519::PublicKey::from_string(issuer).is_err() {
168            return Err(StellarTransactionValidationError::InvalidAssetIdentifier(
169                format!(
170                    "Invalid issuer address format: {issuer} (must be a valid Stellar public key)"
171                ),
172            ));
173        }
174
175        Ok(())
176    }
177
178    /// Validate that an asset identifier is in the allowed tokens list
179    pub fn validate_allowed_token(
180        asset: &str,
181        policy: &RelayerStellarPolicy,
182    ) -> Result<(), StellarTransactionValidationError> {
183        let allowed_tokens = policy.get_allowed_tokens();
184
185        if allowed_tokens.is_empty() {
186            // If no allowed tokens specified, all tokens are allowed
187            return Ok(());
188        }
189
190        // Check if native XLM is allowed
191        if asset == "native" || asset.is_empty() {
192            let native_allowed = allowed_tokens
193                .iter()
194                .any(|token| token.asset == "native" || token.asset.is_empty());
195            if !native_allowed {
196                return Err(StellarTransactionValidationError::TokenNotAllowed(
197                    "Native XLM not in allowed tokens list".to_string(),
198                ));
199            }
200            return Ok(());
201        }
202
203        // Check if the asset is in the allowed list
204        let is_allowed = allowed_tokens.iter().any(|token| token.asset == asset);
205
206        if !is_allowed {
207            return Err(StellarTransactionValidationError::TokenNotAllowed(format!(
208                "Token {asset} not in allowed tokens list"
209            )));
210        }
211
212        Ok(())
213    }
214
215    /// Validate that a fee amount doesn't exceed the maximum allowed fee
216    pub fn validate_max_fee(
217        fee: u64,
218        policy: &RelayerStellarPolicy,
219    ) -> Result<(), StellarTransactionValidationError> {
220        if let Some(max_fee) = policy.max_fee {
221            if fee > max_fee as u64 {
222                return Err(StellarTransactionValidationError::MaxFeeExceeded(fee));
223            }
224        }
225
226        Ok(())
227    }
228
229    /// Validate that a specific token's max_allowed_fee is not exceeded
230    pub fn validate_token_max_fee(
231        asset_id: &str,
232        fee: u64,
233        policy: &RelayerStellarPolicy,
234    ) -> Result<(), StellarTransactionValidationError> {
235        if let Some(token_entry) = policy.get_allowed_token_entry(asset_id) {
236            if let Some(max_allowed_fee) = token_entry.max_allowed_fee {
237                if fee > max_allowed_fee {
238                    return Err(StellarTransactionValidationError::MaxFeeExceeded(fee));
239                }
240            }
241        }
242
243        Ok(())
244    }
245
246    /// Extract payment operations from a transaction envelope that pay to the relayer
247    ///
248    /// Returns a vector of (asset_id, amount) tuples for payments to the relayer
249    pub fn extract_relayer_payments(
250        envelope: &TransactionEnvelope,
251        relayer_address: &str,
252    ) -> Result<Vec<(String, u64)>, StellarTransactionValidationError> {
253        let operations = extract_operations(envelope).map_err(|e| {
254            StellarTransactionValidationError::ValidationError(format!(
255                "Failed to extract operations: {e}"
256            ))
257        })?;
258
259        let mut payments = Vec::new();
260
261        for op in operations.iter() {
262            if let OperationBody::Payment(PaymentOp {
263                destination,
264                asset,
265                amount,
266            }) = &op.body
267            {
268                // Convert destination to string
269                let dest_str = muxed_account_to_string(destination).map_err(|e| {
270                    StellarTransactionValidationError::ValidationError(format!(
271                        "Failed to parse destination: {e}"
272                    ))
273                })?;
274
275                // Check if payment is to relayer
276                if dest_str == relayer_address {
277                    // Convert asset to identifier string
278                    let asset_id = asset_to_asset_id(asset).map_err(|e| {
279                        StellarTransactionValidationError::InvalidAssetIdentifier(format!(
280                            "Failed to convert asset to asset_id: {e}"
281                        ))
282                    })?;
283                    // Validate amount is non-negative before converting from i64 to u64
284                    if *amount < 0 {
285                        return Err(StellarTransactionValidationError::ValidationError(
286                            "Negative payment amount".to_string(),
287                        ));
288                    }
289                    let amount_u64 = *amount as u64;
290                    payments.push((asset_id, amount_u64));
291                }
292            }
293        }
294
295        Ok(payments)
296    }
297
298    /// Validate token payment in transaction
299    ///
300    /// Checks that:
301    /// 1. Payment operation to relayer exists
302    /// 2. Token is in allowed_tokens list
303    /// 3. Payment amount matches expected fee (within tolerance)
304    pub fn validate_token_payment(
305        envelope: &TransactionEnvelope,
306        relayer_address: &str,
307        expected_fee_token: &str,
308        expected_fee_amount: u64,
309        policy: &RelayerStellarPolicy,
310    ) -> Result<(), StellarTransactionValidationError> {
311        // Extract payments to relayer
312        let payments = Self::extract_relayer_payments(envelope, relayer_address)?;
313
314        if payments.is_empty() {
315            return Err(StellarTransactionValidationError::ValidationError(
316                "No payment operation found to relayer".to_string(),
317            ));
318        }
319
320        // Find payment matching the expected token
321        let matching_payment = payments
322            .iter()
323            .find(|(asset_id, _)| asset_id == expected_fee_token);
324
325        match matching_payment {
326            Some((asset_id, amount)) => {
327                // Validate token is allowed
328                Self::validate_allowed_token(asset_id, policy)?;
329
330                // Validate amount matches expected (allow 1% tolerance for rounding)
331                let tolerance = (expected_fee_amount as f64 * 0.01) as u64;
332                if *amount < expected_fee_amount.saturating_sub(tolerance) {
333                    return Err(StellarTransactionValidationError::InsufficientTokenPayment(
334                        expected_fee_amount,
335                        *amount,
336                    ));
337                }
338
339                // Validate max fee
340                Self::validate_token_max_fee(asset_id, *amount, policy)?;
341
342                Ok(())
343            }
344            None => Err(StellarTransactionValidationError::ValidationError(format!(
345                "No payment found for expected token: {expected_fee_token}. Found payments: {payments:?}"
346            ))),
347        }
348    }
349
350    /// Validate that the source account is not the relayer address
351    ///
352    /// This prevents malicious attempts to drain the relayer's funds by
353    /// using the relayer as the transaction source.
354    fn validate_source_account_not_relayer(
355        envelope: &TransactionEnvelope,
356        relayer_address: &str,
357    ) -> Result<(), StellarTransactionValidationError> {
358        let source_account = extract_source_account(envelope).map_err(|e| {
359            StellarTransactionValidationError::ValidationError(format!(
360                "Failed to extract source account: {e}"
361            ))
362        })?;
363
364        if source_account == relayer_address {
365            return Err(StellarTransactionValidationError::ValidationError(
366                "Transaction source account cannot be the relayer address. This is a security measure to prevent relayer fund drainage.".to_string(),
367            ));
368        }
369
370        Ok(())
371    }
372
373    /// Validate transaction type
374    ///
375    /// Rejects fee-bump transactions as they are not suitable for gasless transactions.
376    fn validate_transaction_type(
377        envelope: &TransactionEnvelope,
378    ) -> Result<(), StellarTransactionValidationError> {
379        match envelope {
380            soroban_rs::xdr::TransactionEnvelope::TxFeeBump(_) => {
381                Err(StellarTransactionValidationError::ValidationError(
382                    "Fee-bump transactions are not supported for gasless transactions".to_string(),
383                ))
384            }
385            _ => Ok(()),
386        }
387    }
388
389    /// Validate that operations don't target the relayer (except for fee payment)
390    ///
391    /// This prevents operations that could drain the relayer's funds or manipulate
392    /// the relayer's account state. Fee payment operations are expected and allowed.
393    fn validate_operations_not_targeting_relayer(
394        envelope: &TransactionEnvelope,
395        relayer_address: &str,
396    ) -> Result<(), StellarTransactionValidationError> {
397        let operations = extract_operations(envelope).map_err(|e| {
398            StellarTransactionValidationError::ValidationError(format!(
399                "Failed to extract operations: {e}"
400            ))
401        })?;
402
403        for op in operations.iter() {
404            match &op.body {
405                OperationBody::Payment(PaymentOp { destination, .. }) => {
406                    let dest_str = muxed_account_to_string(destination).map_err(|e| {
407                        StellarTransactionValidationError::ValidationError(format!(
408                            "Failed to parse destination: {e}"
409                        ))
410                    })?;
411
412                    // Payment to relayer is allowed (for fee payment), but we log it
413                    if dest_str == relayer_address {
414                        // This is expected for fee payment, but we should ensure
415                        // it's the last operation added by the relayer
416                        continue;
417                    }
418                }
419                OperationBody::AccountMerge(destination) => {
420                    let dest_str = muxed_account_to_string(destination).map_err(|e| {
421                        StellarTransactionValidationError::ValidationError(format!(
422                            "Failed to parse merge destination: {e}"
423                        ))
424                    })?;
425
426                    if dest_str == relayer_address {
427                        return Err(StellarTransactionValidationError::ValidationError(
428                            "Account merge operations targeting the relayer are not allowed"
429                                .to_string(),
430                        ));
431                    }
432                }
433                OperationBody::SetOptions(_) => {
434                    // SetOptions operations could potentially modify account settings
435                    // We should reject them if they target relayer, but SetOptions doesn't have a target
436                    // However, SetOptions on the source account could be problematic
437                    // For now, we allow SetOptions but could add more specific checks
438                }
439                _ => {
440                    // Other operation types are checked in validate_operation_types
441                }
442            }
443        }
444
445        Ok(())
446    }
447
448    /// Validate operations count
449    ///
450    /// Ensures the transaction has a reasonable number of operations.
451    fn validate_operations_count(
452        envelope: &TransactionEnvelope,
453    ) -> Result<(), StellarTransactionValidationError> {
454        let operations = extract_operations(envelope).map_err(|e| {
455            StellarTransactionValidationError::ValidationError(format!(
456                "Failed to extract operations: {e}"
457            ))
458        })?;
459
460        if operations.is_empty() {
461            return Err(StellarTransactionValidationError::ValidationError(
462                "Transaction must contain at least one operation".to_string(),
463            ));
464        }
465
466        if operations.len() > STELLAR_MAX_OPERATIONS {
467            return Err(StellarTransactionValidationError::ValidationError(format!(
468                "Transaction contains too many operations: {} (maximum is {})",
469                operations.len(),
470                STELLAR_MAX_OPERATIONS
471            )));
472        }
473
474        Ok(())
475    }
476
477    /// Convert AccountId to string representation
478    fn account_id_to_string(
479        account_id: &AccountId,
480    ) -> Result<String, StellarTransactionValidationError> {
481        match &account_id.0 {
482            XdrPublicKey::PublicKeyTypeEd25519(uint256) => {
483                let bytes: [u8; 32] = uint256.0;
484                let pk = PublicKey(bytes);
485                Ok(pk.to_string())
486            }
487        }
488    }
489
490    /// Check if a footprint key targets relayer-owned storage
491    #[allow(dead_code)]
492    fn footprint_key_targets_relayer(
493        key: &LedgerKey,
494        relayer_address: &str,
495    ) -> Result<bool, StellarTransactionValidationError> {
496        match key {
497            LedgerKey::Account(account_key) => {
498                // Extract account ID from the key
499                let account_str = Self::account_id_to_string(&account_key.account_id)?;
500                Ok(account_str == relayer_address)
501            }
502            LedgerKey::Trustline(trustline_key) => {
503                // Check if trustline belongs to relayer
504                let account_str = Self::account_id_to_string(&trustline_key.account_id)?;
505                Ok(account_str == relayer_address)
506            }
507            LedgerKey::ContractData(contract_data_key) => {
508                // Check if contract data key references relayer account
509                match &contract_data_key.contract {
510                    ScAddress::Account(acc_id) => {
511                        let account_str = Self::account_id_to_string(acc_id)?;
512                        Ok(account_str == relayer_address)
513                    }
514                    ScAddress::Contract(_) => {
515                        // Contract storage keys are allowed
516                        Ok(false)
517                    }
518                    ScAddress::MuxedAccount(_)
519                    | ScAddress::ClaimableBalance(_)
520                    | ScAddress::LiquidityPool(_) => {
521                        // These are not account addresses, so they're safe
522                        Ok(false)
523                    }
524                }
525            }
526            LedgerKey::ContractCode(_) => {
527                // Contract code keys are allowed
528                Ok(false)
529            }
530            _ => {
531                // Other ledger key types are allowed
532                Ok(false)
533            }
534        }
535    }
536
537    /// Validate contract invocation operation
538    ///
539    /// Performs comprehensive security validation for Soroban contract invocations:
540    /// 1. Validates host function type is allowed
541    /// 2. Validates Soroban auth entries don't require relayer
542    fn validate_contract_invocation(
543        invoke: &InvokeHostFunctionOp,
544        op_idx: usize,
545        relayer_address: &str,
546        _policy: &RelayerStellarPolicy,
547    ) -> Result<(), StellarTransactionValidationError> {
548        // 1. Validate host function type
549        match &invoke.host_function {
550            HostFunction::InvokeContract(_) => {
551                // Contract invocations are allowed by default
552            }
553            HostFunction::CreateContract(_) => {
554                return Err(StellarTransactionValidationError::ValidationError(format!(
555                    "Op {op_idx}: CreateContract not allowed for gasless transactions"
556                )));
557            }
558            HostFunction::UploadContractWasm(_) => {
559                return Err(StellarTransactionValidationError::ValidationError(format!(
560                    "Op {op_idx}: UploadContractWasm not allowed for gasless transactions"
561                )));
562            }
563            _ => {
564                return Err(StellarTransactionValidationError::ValidationError(format!(
565                    "Op {op_idx}: Unsupported host function"
566                )));
567            }
568        }
569
570        // Validate Soroban auth entries
571        for (i, entry) in invoke.auth.iter().enumerate() {
572            // Validate that relayer is NOT required signer
573            match &entry.credentials {
574                SorobanCredentials::SourceAccount => {
575                    // We've already validated that the source account is not the relayer,
576                    // so SourceAccount credentials are safe.
577                }
578                SorobanCredentials::Address(address_creds) => {
579                    // Check if the address is the relayer
580                    match &address_creds.address {
581                        ScAddress::Account(acc_id) => {
582                            // Convert account ID to string for comparison
583                            let account_str = Self::account_id_to_string(acc_id)?;
584                            if account_str == relayer_address {
585                                return Err(StellarTransactionValidationError::ValidationError(
586                                    format!(
587                                        "Op {op_idx}: Soroban auth entry {i} requires relayer ({relayer_address}). Forbidden."
588                                    ),
589                                ));
590                            }
591                        }
592                        ScAddress::Contract(_) => {
593                            // Contract addresses in auth are allowed
594                        }
595                        ScAddress::MuxedAccount(_) => {
596                            // Muxed accounts are allowed
597                        }
598                        ScAddress::ClaimableBalance(_) | ScAddress::LiquidityPool(_) => {
599                            // These are not account addresses, so they're safe
600                        }
601                    }
602                }
603            }
604        }
605
606        Ok(())
607    }
608
609    /// Validate operation types
610    ///
611    /// Ensures only allowed operation types are present in the transaction.
612    /// Currently allows common operation types but can be extended based on policy.
613    fn validate_operation_types(
614        envelope: &TransactionEnvelope,
615        relayer_address: &str,
616        policy: &RelayerStellarPolicy,
617    ) -> Result<(), StellarTransactionValidationError> {
618        let operations = extract_operations(envelope).map_err(|e| {
619            StellarTransactionValidationError::ValidationError(format!(
620                "Failed to extract operations: {e}"
621            ))
622        })?;
623
624        for (idx, op) in operations.iter().enumerate() {
625            match &op.body {
626                // Prevent account merges (could drain account before payment executes)
627                OperationBody::AccountMerge(_) => {
628                    return Err(StellarTransactionValidationError::ValidationError(format!(
629                        "Operation {idx}: AccountMerge operations are not allowed"
630                    )));
631                }
632
633                // Prevent SetOptions that could lock out the account
634                OperationBody::SetOptions(_set_opts) => {
635                    return Err(StellarTransactionValidationError::ValidationError(format!(
636                        "Operation {idx}: SetOptions operations are not allowed"
637                    )));
638                }
639
640                // Validate smart contract invocations
641                OperationBody::InvokeHostFunction(invoke) => {
642                    Self::validate_contract_invocation(invoke, idx, relayer_address, policy)?;
643                }
644
645                // Allow common operations
646                OperationBody::Payment(_)
647                | OperationBody::PathPaymentStrictReceive(_)
648                | OperationBody::PathPaymentStrictSend(_)
649                | OperationBody::ManageSellOffer(_)
650                | OperationBody::ManageBuyOffer(_)
651                | OperationBody::CreatePassiveSellOffer(_)
652                | OperationBody::ChangeTrust(_)
653                | OperationBody::ManageData(_)
654                | OperationBody::BumpSequence(_)
655                | OperationBody::CreateClaimableBalance(_)
656                | OperationBody::ClaimClaimableBalance(_)
657                | OperationBody::BeginSponsoringFutureReserves(_)
658                | OperationBody::EndSponsoringFutureReserves
659                | OperationBody::RevokeSponsorship(_)
660                | OperationBody::Clawback(_)
661                | OperationBody::ClawbackClaimableBalance(_)
662                | OperationBody::SetTrustLineFlags(_)
663                | OperationBody::LiquidityPoolDeposit(_)
664                | OperationBody::LiquidityPoolWithdraw(_) => {
665                    // These are generally safe
666                }
667
668                // Deprecated operations
669                OperationBody::CreateAccount(_) | OperationBody::AllowTrust(_) => {
670                    return Err(StellarTransactionValidationError::ValidationError(format!(
671                        "Operation {idx}: Deprecated operation type not allowed"
672                    )));
673                }
674
675                // Other operations
676                OperationBody::Inflation
677                | OperationBody::ExtendFootprintTtl(_)
678                | OperationBody::RestoreFootprint(_) => {
679                    // These are allowed
680                }
681            }
682        }
683
684        Ok(())
685    }
686
687    /// Validate sequence number
688    ///
689    /// Validates that the transaction sequence number is valid for the source account.
690    /// Note: The relayer will fee-bump this transaction, so the relayer's sequence will be consumed.
691    /// However, the inner transaction (user's tx) must still have a valid sequence number.
692    ///
693    /// The transaction sequence must be strictly greater than the account's current sequence number.
694    /// Future sequence numbers are allowed (user can queue transactions), but equal sequences are rejected.
695    pub async fn validate_sequence_number<P>(
696        envelope: &TransactionEnvelope,
697        provider: &P,
698    ) -> Result<(), StellarTransactionValidationError>
699    where
700        P: StellarProviderTrait + Send + Sync,
701    {
702        // Extract source account
703        let source_account = extract_source_account(envelope).map_err(|e| {
704            StellarTransactionValidationError::ValidationError(format!(
705                "Failed to extract source account: {e}"
706            ))
707        })?;
708
709        // Get account's current sequence number from chain
710        let account_entry = provider.get_account(&source_account).await.map_err(|e| {
711            StellarTransactionValidationError::ValidationError(format!(
712                "Failed to get account sequence: {e}"
713            ))
714        })?;
715        let account_seq_num = account_entry.seq_num.0;
716
717        // Extract transaction sequence number
718        let tx_seq_num = match envelope {
719            TransactionEnvelope::TxV0(e) => e.tx.seq_num.0,
720            TransactionEnvelope::Tx(e) => e.tx.seq_num.0,
721            TransactionEnvelope::TxFeeBump(_) => {
722                return Err(StellarTransactionValidationError::ValidationError(
723                    "Fee-bump transactions are not supported for gasless transactions".to_string(),
724                ));
725            }
726        };
727
728        // Validate that transaction sequence number is strictly greater than account's current sequence
729        // Stellar requires tx_seq_num > account_seq_num (not >=). Equal sequences are invalid.
730        // The user can set a future sequence number, but not a past or equal one
731        if tx_seq_num <= account_seq_num {
732            return Err(StellarTransactionValidationError::ValidationError(format!(
733                "Transaction sequence number {tx_seq_num} is invalid. Account's current sequence is {account_seq_num}. \
734                The transaction sequence must be strictly greater than the account's current sequence."
735            )));
736        }
737
738        Ok(())
739    }
740
741    /// Comprehensive validation for gasless transactions
742    ///
743    /// Performs all security and policy validations on a transaction envelope
744    /// before it's processed for gasless execution.
745    ///
746    /// This includes:
747    /// - Validating source account is not relayer
748    /// - Validating transaction type
749    /// - Validating operations don't target relayer (except fee payment)
750    /// - Validating operations count
751    /// - Validating operation types
752    /// - Validating sequence number
753    /// - Validating transaction validity duration (if max_validity_duration is provided)
754    ///
755    /// # Arguments
756    /// * `envelope` - The transaction envelope to validate
757    /// * `relayer_address` - The relayer's Stellar address
758    /// * `policy` - The relayer policy
759    /// * `provider` - Provider for Stellar RPC operations
760    /// * `max_validity_duration` - Optional maximum allowed transaction validity duration. If provided,
761    ///   validates that the transaction's time bounds don't exceed this duration. This protects against
762    ///   price fluctuations for user-paid fee transactions.
763    pub async fn gasless_transaction_validation<P>(
764        envelope: &TransactionEnvelope,
765        relayer_address: &str,
766        policy: &RelayerStellarPolicy,
767        provider: &P,
768        max_validity_duration: Option<Duration>,
769    ) -> Result<(), StellarTransactionValidationError>
770    where
771        P: StellarProviderTrait + Send + Sync,
772    {
773        Self::validate_source_account_not_relayer(envelope, relayer_address)?;
774        Self::validate_transaction_type(envelope)?;
775        Self::validate_operations_not_targeting_relayer(envelope, relayer_address)?;
776        Self::validate_operations_count(envelope)?;
777        Self::validate_operation_types(envelope, relayer_address, policy)?;
778        Self::validate_sequence_number(envelope, provider).await?;
779
780        // Validate that transaction time bounds are not expired
781        Self::validate_time_bounds_not_expired(envelope)?;
782
783        // Validate transaction validity duration if max_validity_duration is provided
784        if let Some(max_duration) = max_validity_duration {
785            Self::validate_transaction_validity_duration(envelope, max_duration)?;
786        }
787
788        Ok(())
789    }
790
791    /// Validate that transaction time bounds are valid and not expired
792    ///
793    /// Checks that:
794    /// 1. Time bounds exist (if envelope has them)
795    /// 2. Current time is within the bounds (min_time <= now <= max_time)
796    /// 3. Transaction has not expired (now <= max_time)
797    ///
798    /// # Arguments
799    /// * `envelope` - The transaction envelope to validate
800    ///
801    /// # Returns
802    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
803    pub fn validate_time_bounds_not_expired(
804        envelope: &TransactionEnvelope,
805    ) -> Result<(), StellarTransactionValidationError> {
806        let time_bounds = extract_time_bounds(envelope);
807
808        if let Some(bounds) = time_bounds {
809            let now = Utc::now().timestamp() as u64;
810            let min_time = bounds.min_time.0;
811            let max_time = bounds.max_time.0;
812
813            // Check if transaction has expired
814            // max_time == 0 means unbounded in Stellar (no upper limit)
815            if max_time != 0 && now > max_time {
816                return Err(StellarTransactionValidationError::ValidationError(format!(
817                    "Transaction has expired: max_time={max_time}, current_time={now}"
818                )));
819            }
820
821            // Check if transaction is not yet valid (optional check, but good to have)
822            if min_time > 0 && now < min_time {
823                return Err(StellarTransactionValidationError::ValidationError(format!(
824                    "Transaction is not yet valid: min_time={min_time}, current_time={now}"
825                )));
826            }
827        }
828        // If no time bounds are set, we don't fail here (some transactions may not have them)
829        // The caller can decide if time bounds are required
830
831        Ok(())
832    }
833
834    /// Validate that transaction validity duration is within the maximum allowed time
835    ///
836    /// This prevents price fluctuations and protects the relayer from losses.
837    /// The transaction must have time bounds set and the validity duration must not exceed
838    /// the maximum allowed duration.
839    ///
840    /// # Arguments
841    /// * `envelope` - The transaction envelope to validate
842    /// * `max_duration` - Maximum allowed validity duration
843    ///
844    /// # Returns
845    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
846    pub fn validate_transaction_validity_duration(
847        envelope: &TransactionEnvelope,
848        max_duration: Duration,
849    ) -> Result<(), StellarTransactionValidationError> {
850        let time_bounds = extract_time_bounds(envelope);
851
852        if let Some(bounds) = time_bounds {
853            // max_time == 0 means unbounded in Stellar (no upper limit)
854            // For duration validation, we require a bounded max_time
855            if bounds.max_time.0 == 0 {
856                return Err(StellarTransactionValidationError::ValidationError(
857                    "Transaction has unbounded validity (max_time=0), but bounded validity is required".to_string(),
858                ));
859            }
860
861            let max_time =
862                DateTime::from_timestamp(bounds.max_time.0 as i64, 0).ok_or_else(|| {
863                    StellarTransactionValidationError::ValidationError(
864                        "Invalid max_time in time bounds".to_string(),
865                    )
866                })?;
867            let now = Utc::now();
868            let duration = max_time - now;
869
870            if duration > max_duration {
871                return Err(StellarTransactionValidationError::ValidationError(format!(
872                    "Transaction validity duration ({duration:?}) exceeds maximum allowed duration ({max_duration:?})"
873                )));
874            }
875        } else {
876            return Err(StellarTransactionValidationError::ValidationError(
877                "Transaction must have time bounds set".to_string(),
878            ));
879        }
880
881        Ok(())
882    }
883
884    /// Comprehensive validation for user fee payment transactions
885    ///
886    /// This function performs all validations required for user-paid fee transactions.
887    /// It validates:
888    /// 1. Transaction structure and operations (via gasless_transaction_validation)
889    /// 2. Fee payment operations exist and are valid
890    /// 3. Allowed token validation
891    /// 4. Token max fee validation
892    /// 5. Payment amount is sufficient (compares with required fee including margin)
893    /// 6. Transaction validity duration (if max_validity_duration is provided)
894    ///
895    /// This function is used by both fee-bump and sign-transaction flows.
896    /// For sign-transaction flows, pass `max_validity_duration` to enforce time bounds.
897    /// For fee-bump flows, pass `None` as transactions may not have time bounds set yet.
898    ///
899    /// # Arguments
900    /// * `envelope` - The transaction envelope to validate
901    /// * `relayer_address` - The relayer's Stellar address
902    /// * `policy` - The relayer policy containing fee payment strategy and token settings
903    /// * `provider` - Provider for Stellar RPC operations
904    /// * `dex_service` - DEX service for fetching quotes to validate payment amounts
905    /// * `max_validity_duration` - Optional maximum allowed transaction validity duration.
906    ///   If provided, validates that the transaction's time bounds don't exceed this duration.
907    ///   This protects against price fluctuations for user-paid fee transactions when signing.
908    ///   Pass `None` for fee-bump flows where time bounds may not be set yet.
909    ///
910    /// # Returns
911    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
912    pub async fn validate_user_fee_payment_transaction<P, D>(
913        envelope: &TransactionEnvelope,
914        relayer_address: &str,
915        policy: &RelayerStellarPolicy,
916        provider: &P,
917        dex_service: &D,
918        max_validity_duration: Option<Duration>,
919    ) -> Result<(), StellarTransactionValidationError>
920    where
921        P: StellarProviderTrait + Send + Sync,
922        D: StellarDexServiceTrait + Send + Sync,
923    {
924        // Step 1: Comprehensive security validation for gasless transactions
925        // Include duration validation if max_validity_duration is provided
926        Self::gasless_transaction_validation(
927            envelope,
928            relayer_address,
929            policy,
930            provider,
931            max_validity_duration,
932        )
933        .await?;
934
935        // Step 2: Validate fee payment amounts
936        Self::validate_user_fee_payment_amounts(
937            envelope,
938            relayer_address,
939            policy,
940            provider,
941            dex_service,
942        )
943        .await?;
944
945        Ok(())
946    }
947
948    /// Validate fee payment amounts for user-paid fee transactions
949    ///
950    /// This function validates that the fee payment operation exists, is valid,
951    /// and the payment amount is sufficient. It's separated from the core validation
952    /// to allow reuse in different flows.
953    ///
954    /// # Arguments
955    /// * `envelope` - The transaction envelope to validate
956    /// * `relayer_address` - The relayer's Stellar address
957    /// * `policy` - The relayer policy containing fee payment strategy and token settings
958    /// * `provider` - Provider for Stellar RPC operations
959    /// * `dex_service` - DEX service for fetching quotes to validate payment amounts
960    ///
961    /// # Returns
962    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
963    async fn validate_user_fee_payment_amounts<P, D>(
964        envelope: &TransactionEnvelope,
965        relayer_address: &str,
966        policy: &RelayerStellarPolicy,
967        provider: &P,
968        dex_service: &D,
969    ) -> Result<(), StellarTransactionValidationError>
970    where
971        P: StellarProviderTrait + Send + Sync,
972        D: StellarDexServiceTrait + Send + Sync,
973    {
974        // Extract the fee payment for amount validation
975        let payments = Self::extract_relayer_payments(envelope, relayer_address)?;
976        if payments.is_empty() {
977            return Err(StellarTransactionValidationError::ValidationError(
978                "Gasless transactions must include a fee payment operation to the relayer"
979                    .to_string(),
980            ));
981        }
982
983        // Validate only one fee payment operation
984        if payments.len() > 1 {
985            return Err(StellarTransactionValidationError::ValidationError(format!(
986                "Gasless transactions must include exactly one fee payment operation to the relayer, found {}",
987                payments.len()
988            )));
989        }
990
991        // Extract the single payment
992        let (asset_id, amount) = &payments[0];
993
994        // Validate fee payment token
995        Self::validate_allowed_token(asset_id, policy)?;
996
997        // Validate max fee
998        Self::validate_token_max_fee(asset_id, *amount, policy)?;
999
1000        // Calculate required XLM fee using estimate_fee (handles Soroban transactions correctly)
1001
1002        let mut required_xlm_fee = estimate_fee(envelope, provider, None).await.map_err(|e| {
1003            StellarTransactionValidationError::ValidationError(format!(
1004                "Failed to estimate fee: {e}",
1005            ))
1006        })?;
1007
1008        let is_soroban = xdr_needs_simulation(envelope).unwrap_or(false);
1009        if !is_soroban {
1010            // For regular transactions, fee-bump needs base fee (100 stroops)
1011            required_xlm_fee += STELLAR_DEFAULT_TRANSACTION_FEE as u64;
1012        }
1013
1014        let fee_quote = convert_xlm_fee_to_token(dex_service, policy, required_xlm_fee, asset_id)
1015            .await
1016            .map_err(|e| {
1017                StellarTransactionValidationError::ValidationError(format!(
1018                    "Failed to convert XLM fee to token {asset_id}: {e}",
1019                ))
1020            })?;
1021
1022        // Compare payment amount with required token amount (from convert_xlm_fee_to_token which includes margin)
1023        if *amount < fee_quote.fee_in_token {
1024            return Err(StellarTransactionValidationError::InsufficientTokenPayment(
1025                fee_quote.fee_in_token,
1026                *amount,
1027            ));
1028        }
1029
1030        // Validate user token balance
1031        Self::validate_user_token_balance(envelope, asset_id, fee_quote.fee_in_token, provider)
1032            .await?;
1033
1034        Ok(())
1035    }
1036
1037    /// Validate that user has sufficient token balance to pay the transaction fee
1038    ///
1039    /// This function checks that the user's account has enough balance of the specified
1040    /// fee token to cover the required transaction fee. This prevents users from getting
1041    /// quotes or building transactions they cannot afford.
1042    ///
1043    /// # Arguments
1044    /// * `envelope` - The transaction envelope to extract source account from
1045    /// * `fee_token` - The token identifier (e.g., "native" or "USDC:GA5Z...")
1046    /// * `required_fee_amount` - The required fee amount in token's smallest unit (stroops)
1047    /// * `provider` - Provider for Stellar RPC operations to fetch balance
1048    ///
1049    /// # Returns
1050    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
1051    pub async fn validate_user_token_balance<P>(
1052        envelope: &TransactionEnvelope,
1053        fee_token: &str,
1054        required_fee_amount: u64,
1055        provider: &P,
1056    ) -> Result<(), StellarTransactionValidationError>
1057    where
1058        P: StellarProviderTrait + Send + Sync,
1059    {
1060        // Extract source account from envelope
1061        let source_account = extract_source_account(envelope).map_err(|e| {
1062            StellarTransactionValidationError::ValidationError(format!(
1063                "Failed to extract source account: {e}"
1064            ))
1065        })?;
1066
1067        // Fetch user's token balance
1068        let user_balance = get_token_balance(provider, &source_account, fee_token)
1069            .await
1070            .map_err(|e| {
1071                StellarTransactionValidationError::ValidationError(format!(
1072                    "Failed to fetch user balance for token {fee_token}: {e}",
1073                ))
1074            })?;
1075
1076        // Check if balance is sufficient
1077        if user_balance < required_fee_amount {
1078            return Err(StellarTransactionValidationError::ValidationError(format!(
1079                "Insufficient balance: user has {user_balance} {fee_token} but needs {required_fee_amount} {fee_token} for transaction fee"
1080            )));
1081        }
1082
1083        Ok(())
1084    }
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089    use super::*;
1090    use crate::domain::transaction::stellar::test_helpers::{
1091        create_account_id, create_muxed_account, create_native_payment_operation,
1092        create_simple_v1_envelope, TEST_CONTRACT, TEST_PK, TEST_PK_2,
1093    };
1094    use crate::models::{AssetSpec, StellarAllowedTokensPolicy};
1095    use crate::services::provider::MockStellarProviderTrait;
1096    use crate::services::stellar_dex::MockStellarDexServiceTrait;
1097    use futures::future::ready;
1098    use soroban_rs::xdr::{
1099        AccountEntry, AccountEntryExt, Asset as XdrAsset, ChangeTrustAsset, ChangeTrustOp,
1100        HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Operation, OperationBody,
1101        ScAddress, ScSymbol, SequenceNumber, SorobanAuthorizationEntry, SorobanAuthorizedFunction,
1102        SorobanCredentials, Thresholds, TimeBounds, TimePoint, Transaction, TransactionEnvelope,
1103        TransactionExt, TransactionV1Envelope,
1104    };
1105
1106    #[test]
1107    fn test_empty_operations_rejected() {
1108        let result = validate_operations(&[]);
1109        assert!(result.is_err());
1110        assert!(result
1111            .unwrap_err()
1112            .to_string()
1113            .contains("at least one operation"));
1114    }
1115
1116    #[test]
1117    fn test_too_many_operations_rejected() {
1118        let ops = vec![
1119            OperationSpec::Payment {
1120                destination: TEST_PK.to_string(),
1121                amount: 1000,
1122                asset: AssetSpec::Native,
1123            };
1124            101
1125        ];
1126        let result = validate_operations(&ops);
1127        assert!(result.is_err());
1128        assert!(result
1129            .unwrap_err()
1130            .to_string()
1131            .contains("maximum allowed is 100"));
1132    }
1133
1134    #[test]
1135    fn test_soroban_exclusivity_enforced() {
1136        // Multiple Soroban operations should fail
1137        let ops = vec![
1138            OperationSpec::InvokeContract {
1139                contract_address: TEST_CONTRACT.to_string(),
1140                function_name: "test".to_string(),
1141                args: vec![],
1142                auth: None,
1143            },
1144            OperationSpec::CreateContract {
1145                source: crate::models::ContractSource::Address {
1146                    address: TEST_PK.to_string(),
1147                },
1148                wasm_hash: "abc123".to_string(),
1149                salt: None,
1150                constructor_args: None,
1151                auth: None,
1152            },
1153        ];
1154        let result = validate_operations(&ops);
1155        assert!(result.is_err());
1156
1157        // Soroban mixed with non-Soroban should fail
1158        let ops = vec![
1159            OperationSpec::InvokeContract {
1160                contract_address: TEST_CONTRACT.to_string(),
1161                function_name: "test".to_string(),
1162                args: vec![],
1163                auth: None,
1164            },
1165            OperationSpec::Payment {
1166                destination: TEST_PK.to_string(),
1167                amount: 1000,
1168                asset: AssetSpec::Native,
1169            },
1170        ];
1171        let result = validate_operations(&ops);
1172        assert!(result.is_err());
1173        assert!(result
1174            .unwrap_err()
1175            .to_string()
1176            .contains("Soroban operations must be exclusive"));
1177    }
1178
1179    #[test]
1180    fn test_soroban_memo_restriction() {
1181        let soroban_op = vec![OperationSpec::InvokeContract {
1182            contract_address: TEST_CONTRACT.to_string(),
1183            function_name: "test".to_string(),
1184            args: vec![],
1185            auth: None,
1186        }];
1187
1188        // Soroban with text memo should fail
1189        let result = validate_soroban_memo_restriction(
1190            &soroban_op,
1191            &Some(MemoSpec::Text {
1192                value: "test".to_string(),
1193            }),
1194        );
1195        assert!(result.is_err());
1196
1197        // Soroban with MemoNone should succeed
1198        let result = validate_soroban_memo_restriction(&soroban_op, &Some(MemoSpec::None));
1199        assert!(result.is_ok());
1200
1201        // Soroban with no memo should succeed
1202        let result = validate_soroban_memo_restriction(&soroban_op, &None);
1203        assert!(result.is_ok());
1204    }
1205
1206    mod validate_fee_token_structure_tests {
1207        use super::*;
1208
1209        #[test]
1210        fn test_native_xlm_valid() {
1211            assert!(StellarTransactionValidator::validate_fee_token_structure("native").is_ok());
1212            assert!(StellarTransactionValidator::validate_fee_token_structure("XLM").is_ok());
1213            assert!(StellarTransactionValidator::validate_fee_token_structure("").is_ok());
1214        }
1215
1216        #[test]
1217        fn test_contract_address_valid() {
1218            assert!(
1219                StellarTransactionValidator::validate_fee_token_structure(TEST_CONTRACT).is_ok()
1220            );
1221        }
1222
1223        #[test]
1224        fn test_contract_address_invalid_length() {
1225            let result = StellarTransactionValidator::validate_fee_token_structure("C123");
1226            assert!(result.is_err());
1227            assert!(result
1228                .unwrap_err()
1229                .to_string()
1230                .contains("Invalid fee_token format"));
1231        }
1232
1233        #[test]
1234        fn test_classic_asset_valid() {
1235            let result = StellarTransactionValidator::validate_fee_token_structure(&format!(
1236                "USDC:{}",
1237                TEST_PK
1238            ));
1239            assert!(result.is_ok());
1240        }
1241
1242        #[test]
1243        fn test_classic_asset_code_too_long() {
1244            let result = StellarTransactionValidator::validate_fee_token_structure(&format!(
1245                "VERYLONGCODE1:{}",
1246                TEST_PK
1247            ));
1248            assert!(result.is_err());
1249            assert!(result
1250                .unwrap_err()
1251                .to_string()
1252                .contains("Invalid asset code length"));
1253        }
1254
1255        #[test]
1256        fn test_classic_asset_invalid_issuer_length() {
1257            let result = StellarTransactionValidator::validate_fee_token_structure("USDC:GSHORT");
1258            assert!(result.is_err());
1259            assert!(result
1260                .unwrap_err()
1261                .to_string()
1262                .contains("Invalid issuer address length"));
1263        }
1264
1265        #[test]
1266        fn test_classic_asset_invalid_issuer_prefix() {
1267            let result = StellarTransactionValidator::validate_fee_token_structure(
1268                "USDC:SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
1269            );
1270            assert!(result.is_err());
1271            assert!(result
1272                .unwrap_err()
1273                .to_string()
1274                .contains("Invalid issuer address prefix"));
1275        }
1276
1277        #[test]
1278        fn test_invalid_format_multiple_colons() {
1279            let result =
1280                StellarTransactionValidator::validate_fee_token_structure("USDC:ISSUER:EXTRA");
1281            assert!(result.is_err());
1282            assert!(result
1283                .unwrap_err()
1284                .to_string()
1285                .contains("Invalid fee_token format"));
1286        }
1287    }
1288
1289    mod validate_allowed_token_tests {
1290        use super::*;
1291
1292        #[test]
1293        fn test_empty_allowed_list_allows_all() {
1294            let policy = RelayerStellarPolicy::default();
1295            assert!(StellarTransactionValidator::validate_allowed_token("native", &policy).is_ok());
1296            assert!(
1297                StellarTransactionValidator::validate_allowed_token(TEST_CONTRACT, &policy).is_ok()
1298            );
1299        }
1300
1301        #[test]
1302        fn test_native_allowed() {
1303            let mut policy = RelayerStellarPolicy::default();
1304            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1305                asset: "native".to_string(),
1306                metadata: None,
1307                swap_config: None,
1308                max_allowed_fee: None,
1309            }]);
1310            assert!(StellarTransactionValidator::validate_allowed_token("native", &policy).is_ok());
1311            assert!(StellarTransactionValidator::validate_allowed_token("", &policy).is_ok());
1312        }
1313
1314        #[test]
1315        fn test_native_not_allowed() {
1316            let mut policy = RelayerStellarPolicy::default();
1317            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1318                asset: format!("USDC:{}", TEST_PK),
1319                metadata: None,
1320                swap_config: None,
1321                max_allowed_fee: None,
1322            }]);
1323            let result = StellarTransactionValidator::validate_allowed_token("native", &policy);
1324            assert!(result.is_err());
1325            assert!(result
1326                .unwrap_err()
1327                .to_string()
1328                .contains("Native XLM not in allowed tokens list"));
1329        }
1330
1331        #[test]
1332        fn test_token_allowed() {
1333            let token = format!("USDC:{}", TEST_PK);
1334            let mut policy = RelayerStellarPolicy::default();
1335            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1336                asset: token.clone(),
1337                metadata: None,
1338                swap_config: None,
1339                max_allowed_fee: None,
1340            }]);
1341            assert!(StellarTransactionValidator::validate_allowed_token(&token, &policy).is_ok());
1342        }
1343
1344        #[test]
1345        fn test_token_not_allowed() {
1346            let mut policy = RelayerStellarPolicy::default();
1347            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1348                asset: format!("USDC:{}", TEST_PK),
1349                metadata: None,
1350                swap_config: None,
1351                max_allowed_fee: None,
1352            }]);
1353            let result = StellarTransactionValidator::validate_allowed_token(
1354                &format!("AQUA:{}", TEST_PK_2),
1355                &policy,
1356            );
1357            assert!(result.is_err());
1358            assert!(result
1359                .unwrap_err()
1360                .to_string()
1361                .contains("not in allowed tokens list"));
1362        }
1363    }
1364
1365    mod validate_max_fee_tests {
1366        use super::*;
1367
1368        #[test]
1369        fn test_no_max_fee_allows_any() {
1370            let policy = RelayerStellarPolicy::default();
1371            assert!(StellarTransactionValidator::validate_max_fee(1_000_000, &policy).is_ok());
1372        }
1373
1374        #[test]
1375        fn test_fee_within_limit() {
1376            let mut policy = RelayerStellarPolicy::default();
1377            policy.max_fee = Some(1_000_000);
1378            assert!(StellarTransactionValidator::validate_max_fee(500_000, &policy).is_ok());
1379        }
1380
1381        #[test]
1382        fn test_fee_exceeds_limit() {
1383            let mut policy = RelayerStellarPolicy::default();
1384            policy.max_fee = Some(1_000_000);
1385            let result = StellarTransactionValidator::validate_max_fee(2_000_000, &policy);
1386            assert!(result.is_err());
1387            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
1388        }
1389    }
1390
1391    mod validate_token_max_fee_tests {
1392        use super::*;
1393
1394        #[test]
1395        fn test_no_token_entry() {
1396            let policy = RelayerStellarPolicy::default();
1397            assert!(StellarTransactionValidator::validate_token_max_fee(
1398                "USDC:ISSUER",
1399                1_000_000,
1400                &policy
1401            )
1402            .is_ok());
1403        }
1404
1405        #[test]
1406        fn test_no_max_allowed_fee_in_entry() {
1407            let mut policy = RelayerStellarPolicy::default();
1408            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1409                asset: "USDC:ISSUER".to_string(),
1410                metadata: None,
1411                swap_config: None,
1412                max_allowed_fee: None,
1413            }]);
1414            assert!(StellarTransactionValidator::validate_token_max_fee(
1415                "USDC:ISSUER",
1416                1_000_000,
1417                &policy
1418            )
1419            .is_ok());
1420        }
1421
1422        #[test]
1423        fn test_fee_within_token_limit() {
1424            let mut policy = RelayerStellarPolicy::default();
1425            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1426                asset: "USDC:ISSUER".to_string(),
1427                metadata: None,
1428                swap_config: None,
1429                max_allowed_fee: Some(1_000_000),
1430            }]);
1431            assert!(StellarTransactionValidator::validate_token_max_fee(
1432                "USDC:ISSUER",
1433                500_000,
1434                &policy
1435            )
1436            .is_ok());
1437        }
1438
1439        #[test]
1440        fn test_fee_exceeds_token_limit() {
1441            let mut policy = RelayerStellarPolicy::default();
1442            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1443                asset: "USDC:ISSUER".to_string(),
1444                metadata: None,
1445                swap_config: None,
1446                max_allowed_fee: Some(1_000_000),
1447            }]);
1448            let result = StellarTransactionValidator::validate_token_max_fee(
1449                "USDC:ISSUER",
1450                2_000_000,
1451                &policy,
1452            );
1453            assert!(result.is_err());
1454            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
1455        }
1456    }
1457
1458    mod extract_relayer_payments_tests {
1459        use super::*;
1460
1461        #[test]
1462        fn test_extract_single_payment() {
1463            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1464            let payments =
1465                StellarTransactionValidator::extract_relayer_payments(&envelope, TEST_PK_2)
1466                    .unwrap();
1467            assert_eq!(payments.len(), 1);
1468            assert_eq!(payments[0].0, "native");
1469            assert_eq!(payments[0].1, 1_000_000);
1470        }
1471
1472        #[test]
1473        fn test_extract_no_payments_to_relayer() {
1474            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1475            let payments =
1476                StellarTransactionValidator::extract_relayer_payments(&envelope, TEST_PK).unwrap();
1477            assert_eq!(payments.len(), 0);
1478        }
1479
1480        #[test]
1481        fn test_extract_negative_amount_rejected() {
1482            let payment_op = Operation {
1483                source_account: None,
1484                body: OperationBody::Payment(soroban_rs::xdr::PaymentOp {
1485                    destination: create_muxed_account(TEST_PK_2),
1486                    asset: XdrAsset::Native,
1487                    amount: -100, // Negative amount
1488                }),
1489            };
1490
1491            let tx = Transaction {
1492                source_account: create_muxed_account(TEST_PK),
1493                fee: 100,
1494                seq_num: SequenceNumber(1),
1495                cond: soroban_rs::xdr::Preconditions::None,
1496                memo: soroban_rs::xdr::Memo::None,
1497                operations: vec![payment_op].try_into().unwrap(),
1498                ext: TransactionExt::V0,
1499            };
1500
1501            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1502                tx,
1503                signatures: vec![].try_into().unwrap(),
1504            });
1505
1506            let result =
1507                StellarTransactionValidator::extract_relayer_payments(&envelope, TEST_PK_2);
1508            assert!(result.is_err());
1509            assert!(result
1510                .unwrap_err()
1511                .to_string()
1512                .contains("Negative payment amount"));
1513        }
1514    }
1515
1516    mod validate_time_bounds_tests {
1517        use super::*;
1518
1519        #[test]
1520        fn test_no_time_bounds_is_ok() {
1521            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1522            assert!(
1523                StellarTransactionValidator::validate_time_bounds_not_expired(&envelope).is_ok()
1524            );
1525        }
1526
1527        #[test]
1528        fn test_valid_time_bounds() {
1529            let now = Utc::now().timestamp() as u64;
1530            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1531
1532            let tx = Transaction {
1533                source_account: create_muxed_account(TEST_PK),
1534                fee: 100,
1535                seq_num: SequenceNumber(1),
1536                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1537                    min_time: TimePoint(now - 60),
1538                    max_time: TimePoint(now + 60),
1539                }),
1540                memo: soroban_rs::xdr::Memo::None,
1541                operations: vec![payment_op].try_into().unwrap(),
1542                ext: TransactionExt::V0,
1543            };
1544
1545            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1546                tx,
1547                signatures: vec![].try_into().unwrap(),
1548            });
1549
1550            assert!(
1551                StellarTransactionValidator::validate_time_bounds_not_expired(&envelope).is_ok()
1552            );
1553        }
1554
1555        #[test]
1556        fn test_expired_transaction() {
1557            let now = Utc::now().timestamp() as u64;
1558            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1559
1560            let tx = Transaction {
1561                source_account: create_muxed_account(TEST_PK),
1562                fee: 100,
1563                seq_num: SequenceNumber(1),
1564                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1565                    min_time: TimePoint(now - 120),
1566                    max_time: TimePoint(now - 60), // Expired
1567                }),
1568                memo: soroban_rs::xdr::Memo::None,
1569                operations: vec![payment_op].try_into().unwrap(),
1570                ext: TransactionExt::V0,
1571            };
1572
1573            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1574                tx,
1575                signatures: vec![].try_into().unwrap(),
1576            });
1577
1578            let result = StellarTransactionValidator::validate_time_bounds_not_expired(&envelope);
1579            assert!(result.is_err());
1580            assert!(result.unwrap_err().to_string().contains("has expired"));
1581        }
1582
1583        #[test]
1584        fn test_not_yet_valid_transaction() {
1585            let now = Utc::now().timestamp() as u64;
1586            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1587
1588            let tx = Transaction {
1589                source_account: create_muxed_account(TEST_PK),
1590                fee: 100,
1591                seq_num: SequenceNumber(1),
1592                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1593                    min_time: TimePoint(now + 60), // Not yet valid
1594                    max_time: TimePoint(now + 120),
1595                }),
1596                memo: soroban_rs::xdr::Memo::None,
1597                operations: vec![payment_op].try_into().unwrap(),
1598                ext: TransactionExt::V0,
1599            };
1600
1601            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1602                tx,
1603                signatures: vec![].try_into().unwrap(),
1604            });
1605
1606            let result = StellarTransactionValidator::validate_time_bounds_not_expired(&envelope);
1607            assert!(result.is_err());
1608            assert!(result.unwrap_err().to_string().contains("not yet valid"));
1609        }
1610    }
1611
1612    mod validate_transaction_validity_duration_tests {
1613        use super::*;
1614
1615        #[test]
1616        fn test_duration_within_limit() {
1617            let now = Utc::now().timestamp() as u64;
1618            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1619
1620            let tx = Transaction {
1621                source_account: create_muxed_account(TEST_PK),
1622                fee: 100,
1623                seq_num: SequenceNumber(1),
1624                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1625                    min_time: TimePoint(0),
1626                    max_time: TimePoint(now + 60), // 1 minute from now
1627                }),
1628                memo: soroban_rs::xdr::Memo::None,
1629                operations: vec![payment_op].try_into().unwrap(),
1630                ext: TransactionExt::V0,
1631            };
1632
1633            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1634                tx,
1635                signatures: vec![].try_into().unwrap(),
1636            });
1637
1638            let max_duration = Duration::minutes(5);
1639            assert!(
1640                StellarTransactionValidator::validate_transaction_validity_duration(
1641                    &envelope,
1642                    max_duration
1643                )
1644                .is_ok()
1645            );
1646        }
1647
1648        #[test]
1649        fn test_duration_exceeds_limit() {
1650            let now = Utc::now().timestamp() as u64;
1651            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1652
1653            let tx = Transaction {
1654                source_account: create_muxed_account(TEST_PK),
1655                fee: 100,
1656                seq_num: SequenceNumber(1),
1657                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1658                    min_time: TimePoint(0),
1659                    max_time: TimePoint(now + 600), // 10 minutes from now
1660                }),
1661                memo: soroban_rs::xdr::Memo::None,
1662                operations: vec![payment_op].try_into().unwrap(),
1663                ext: TransactionExt::V0,
1664            };
1665
1666            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1667                tx,
1668                signatures: vec![].try_into().unwrap(),
1669            });
1670
1671            let max_duration = Duration::minutes(5);
1672            let result = StellarTransactionValidator::validate_transaction_validity_duration(
1673                &envelope,
1674                max_duration,
1675            );
1676            assert!(result.is_err());
1677            assert!(result
1678                .unwrap_err()
1679                .to_string()
1680                .contains("exceeds maximum allowed duration"));
1681        }
1682
1683        #[test]
1684        fn test_no_time_bounds_rejected() {
1685            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1686            let max_duration = Duration::minutes(5);
1687            let result = StellarTransactionValidator::validate_transaction_validity_duration(
1688                &envelope,
1689                max_duration,
1690            );
1691            assert!(result.is_err());
1692            assert!(result
1693                .unwrap_err()
1694                .to_string()
1695                .contains("must have time bounds set"));
1696        }
1697    }
1698
1699    mod validate_sequence_number_tests {
1700        use super::*;
1701
1702        #[tokio::test]
1703        async fn test_valid_sequence_number() {
1704            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1705
1706            let mut provider = MockStellarProviderTrait::new();
1707            provider.expect_get_account().returning(|_| {
1708                Box::pin(ready(Ok(AccountEntry {
1709                    account_id: create_account_id(TEST_PK),
1710                    balance: 1_000_000_000,
1711                    seq_num: SequenceNumber(0), // Current sequence is 0, tx sequence is 1
1712                    num_sub_entries: 0,
1713                    inflation_dest: None,
1714                    flags: 0,
1715                    home_domain: Default::default(),
1716                    thresholds: Thresholds([0; 4]),
1717                    signers: Default::default(),
1718                    ext: AccountEntryExt::V0,
1719                })))
1720            });
1721
1722            assert!(
1723                StellarTransactionValidator::validate_sequence_number(&envelope, &provider)
1724                    .await
1725                    .is_ok()
1726            );
1727        }
1728
1729        #[tokio::test]
1730        async fn test_equal_sequence_rejected() {
1731            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1732
1733            let mut provider = MockStellarProviderTrait::new();
1734            provider.expect_get_account().returning(|_| {
1735                Box::pin(ready(Ok(AccountEntry {
1736                    account_id: create_account_id(TEST_PK),
1737                    balance: 1_000_000_000,
1738                    seq_num: SequenceNumber(1), // Same as tx sequence
1739                    num_sub_entries: 0,
1740                    inflation_dest: None,
1741                    flags: 0,
1742                    home_domain: Default::default(),
1743                    thresholds: Thresholds([0; 4]),
1744                    signers: Default::default(),
1745                    ext: AccountEntryExt::V0,
1746                })))
1747            });
1748
1749            let result =
1750                StellarTransactionValidator::validate_sequence_number(&envelope, &provider).await;
1751            assert!(result.is_err());
1752            assert!(result
1753                .unwrap_err()
1754                .to_string()
1755                .contains("strictly greater than"));
1756        }
1757
1758        #[tokio::test]
1759        async fn test_past_sequence_rejected() {
1760            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1761
1762            let mut provider = MockStellarProviderTrait::new();
1763            provider.expect_get_account().returning(|_| {
1764                Box::pin(ready(Ok(AccountEntry {
1765                    account_id: create_account_id(TEST_PK),
1766                    balance: 1_000_000_000,
1767                    seq_num: SequenceNumber(10), // Higher than tx sequence
1768                    num_sub_entries: 0,
1769                    inflation_dest: None,
1770                    flags: 0,
1771                    home_domain: Default::default(),
1772                    thresholds: Thresholds([0; 4]),
1773                    signers: Default::default(),
1774                    ext: AccountEntryExt::V0,
1775                })))
1776            });
1777
1778            let result =
1779                StellarTransactionValidator::validate_sequence_number(&envelope, &provider).await;
1780            assert!(result.is_err());
1781            assert!(result.unwrap_err().to_string().contains("is invalid"));
1782        }
1783    }
1784
1785    mod validate_operations_count_tests {
1786        use super::*;
1787
1788        #[test]
1789        fn test_valid_operations_count() {
1790            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1791            assert!(StellarTransactionValidator::validate_operations_count(&envelope).is_ok());
1792        }
1793
1794        #[test]
1795        fn test_too_many_operations() {
1796            // VecM has a max of 100, so we can't actually create an envelope with 101 operations
1797            // Instead, we test that the validation logic works correctly by checking the limit
1798            // This test verifies the validation function would reject if it could receive such an envelope
1799
1800            // Create an envelope with exactly 100 operations (the maximum)
1801            let operations: Vec<Operation> = (0..100)
1802                .map(|_| create_native_payment_operation(TEST_PK_2, 100))
1803                .collect();
1804
1805            let tx = Transaction {
1806                source_account: create_muxed_account(TEST_PK),
1807                fee: 100,
1808                seq_num: SequenceNumber(1),
1809                cond: soroban_rs::xdr::Preconditions::None,
1810                memo: soroban_rs::xdr::Memo::None,
1811                operations: operations.try_into().unwrap(),
1812                ext: TransactionExt::V0,
1813            };
1814
1815            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1816                tx,
1817                signatures: vec![].try_into().unwrap(),
1818            });
1819
1820            // 100 operations should be OK
1821            let result = StellarTransactionValidator::validate_operations_count(&envelope);
1822            assert!(result.is_ok());
1823        }
1824    }
1825
1826    mod validate_source_account_tests {
1827        use super::*;
1828
1829        #[test]
1830        fn test_source_account_not_relayer() {
1831            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1832            assert!(
1833                StellarTransactionValidator::validate_source_account_not_relayer(
1834                    &envelope, TEST_PK_2
1835                )
1836                .is_ok()
1837            );
1838        }
1839
1840        #[test]
1841        fn test_source_account_is_relayer_rejected() {
1842            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1843            let result = StellarTransactionValidator::validate_source_account_not_relayer(
1844                &envelope, TEST_PK,
1845            );
1846            assert!(result.is_err());
1847            assert!(result
1848                .unwrap_err()
1849                .to_string()
1850                .contains("cannot be the relayer address"));
1851        }
1852    }
1853
1854    mod validate_operation_types_tests {
1855        use super::*;
1856
1857        #[test]
1858        fn test_payment_operation_allowed() {
1859            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1860            let policy = RelayerStellarPolicy::default();
1861            assert!(StellarTransactionValidator::validate_operation_types(
1862                &envelope, TEST_PK_2, &policy
1863            )
1864            .is_ok());
1865        }
1866
1867        #[test]
1868        fn test_account_merge_rejected() {
1869            let operation = Operation {
1870                source_account: None,
1871                body: OperationBody::AccountMerge(create_muxed_account(TEST_PK_2)),
1872            };
1873
1874            let tx = Transaction {
1875                source_account: create_muxed_account(TEST_PK),
1876                fee: 100,
1877                seq_num: SequenceNumber(1),
1878                cond: soroban_rs::xdr::Preconditions::None,
1879                memo: soroban_rs::xdr::Memo::None,
1880                operations: vec![operation].try_into().unwrap(),
1881                ext: TransactionExt::V0,
1882            };
1883
1884            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1885                tx,
1886                signatures: vec![].try_into().unwrap(),
1887            });
1888
1889            let policy = RelayerStellarPolicy::default();
1890            let result = StellarTransactionValidator::validate_operation_types(
1891                &envelope, TEST_PK_2, &policy,
1892            );
1893            assert!(result.is_err());
1894            assert!(result
1895                .unwrap_err()
1896                .to_string()
1897                .contains("AccountMerge operations are not allowed"));
1898        }
1899
1900        #[test]
1901        fn test_set_options_rejected() {
1902            let operation = Operation {
1903                source_account: None,
1904                body: OperationBody::SetOptions(soroban_rs::xdr::SetOptionsOp {
1905                    inflation_dest: None,
1906                    clear_flags: None,
1907                    set_flags: None,
1908                    master_weight: None,
1909                    low_threshold: None,
1910                    med_threshold: None,
1911                    high_threshold: None,
1912                    home_domain: None,
1913                    signer: None,
1914                }),
1915            };
1916
1917            let tx = Transaction {
1918                source_account: create_muxed_account(TEST_PK),
1919                fee: 100,
1920                seq_num: SequenceNumber(1),
1921                cond: soroban_rs::xdr::Preconditions::None,
1922                memo: soroban_rs::xdr::Memo::None,
1923                operations: vec![operation].try_into().unwrap(),
1924                ext: TransactionExt::V0,
1925            };
1926
1927            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1928                tx,
1929                signatures: vec![].try_into().unwrap(),
1930            });
1931
1932            let policy = RelayerStellarPolicy::default();
1933            let result = StellarTransactionValidator::validate_operation_types(
1934                &envelope, TEST_PK_2, &policy,
1935            );
1936            assert!(result.is_err());
1937            assert!(result
1938                .unwrap_err()
1939                .to_string()
1940                .contains("SetOptions operations are not allowed"));
1941        }
1942
1943        #[test]
1944        fn test_change_trust_allowed() {
1945            let operation = Operation {
1946                source_account: None,
1947                body: OperationBody::ChangeTrust(ChangeTrustOp {
1948                    line: ChangeTrustAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
1949                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
1950                        issuer: create_account_id(TEST_PK_2),
1951                    }),
1952                    limit: 1_000_000_000,
1953                }),
1954            };
1955
1956            let tx = Transaction {
1957                source_account: create_muxed_account(TEST_PK),
1958                fee: 100,
1959                seq_num: SequenceNumber(1),
1960                cond: soroban_rs::xdr::Preconditions::None,
1961                memo: soroban_rs::xdr::Memo::None,
1962                operations: vec![operation].try_into().unwrap(),
1963                ext: TransactionExt::V0,
1964            };
1965
1966            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1967                tx,
1968                signatures: vec![].try_into().unwrap(),
1969            });
1970
1971            let policy = RelayerStellarPolicy::default();
1972            assert!(StellarTransactionValidator::validate_operation_types(
1973                &envelope, TEST_PK_2, &policy
1974            )
1975            .is_ok());
1976        }
1977    }
1978
1979    mod validate_token_payment_tests {
1980        use super::*;
1981
1982        #[test]
1983        fn test_valid_native_payment() {
1984            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1985            let policy = RelayerStellarPolicy::default();
1986
1987            let result = StellarTransactionValidator::validate_token_payment(
1988                &envelope, TEST_PK_2, "native", 1_000_000, &policy,
1989            );
1990            assert!(result.is_ok());
1991        }
1992
1993        #[test]
1994        fn test_no_payment_to_relayer() {
1995            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1996            let policy = RelayerStellarPolicy::default();
1997
1998            // Wrong relayer address - no payments will match
1999            let result = StellarTransactionValidator::validate_token_payment(
2000                &envelope, TEST_PK, // Different from destination
2001                "native", 1_000_000, &policy,
2002            );
2003            assert!(result.is_err());
2004            assert!(result
2005                .unwrap_err()
2006                .to_string()
2007                .contains("No payment operation found to relayer"));
2008        }
2009
2010        #[test]
2011        fn test_wrong_token_in_payment() {
2012            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2013            let policy = RelayerStellarPolicy::default();
2014
2015            // Expecting USDC but envelope has native payment
2016            let result = StellarTransactionValidator::validate_token_payment(
2017                &envelope,
2018                TEST_PK_2,
2019                &format!("USDC:{}", TEST_PK),
2020                1_000_000,
2021                &policy,
2022            );
2023            assert!(result.is_err());
2024            assert!(result
2025                .unwrap_err()
2026                .to_string()
2027                .contains("No payment found for expected token"));
2028        }
2029
2030        #[test]
2031        fn test_insufficient_payment_amount() {
2032            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2033            let policy = RelayerStellarPolicy::default();
2034
2035            // Expecting 2M but envelope has 1M payment
2036            let result = StellarTransactionValidator::validate_token_payment(
2037                &envelope, TEST_PK_2, "native", 2_000_000, &policy,
2038            );
2039            assert!(result.is_err());
2040            assert!(result
2041                .unwrap_err()
2042                .to_string()
2043                .contains("Insufficient token payment"));
2044        }
2045
2046        #[test]
2047        fn test_payment_within_tolerance() {
2048            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2049            let policy = RelayerStellarPolicy::default();
2050
2051            let result = StellarTransactionValidator::validate_token_payment(
2052                &envelope, TEST_PK_2, "native", 990_000, &policy,
2053            );
2054            assert!(result.is_ok());
2055        }
2056
2057        #[test]
2058        fn test_token_not_in_allowed_list() {
2059            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2060            let mut policy = RelayerStellarPolicy::default();
2061            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2062                asset: format!("USDC:{}", TEST_PK),
2063                metadata: None,
2064                swap_config: None,
2065                max_allowed_fee: None,
2066            }]);
2067
2068            // Native payment but only USDC is allowed
2069            let result = StellarTransactionValidator::validate_token_payment(
2070                &envelope, TEST_PK_2, "native", 1_000_000, &policy,
2071            );
2072            assert!(result.is_err());
2073            assert!(result
2074                .unwrap_err()
2075                .to_string()
2076                .contains("not in allowed tokens list"));
2077        }
2078
2079        #[test]
2080        fn test_payment_exceeds_token_max_fee() {
2081            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2082            let mut policy = RelayerStellarPolicy::default();
2083            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2084                asset: "native".to_string(),
2085                metadata: None,
2086                swap_config: None,
2087                max_allowed_fee: Some(500_000), // Max 0.5 XLM
2088            }]);
2089
2090            // Payment is 1M but max allowed is 500K
2091            let result = StellarTransactionValidator::validate_token_payment(
2092                &envelope, TEST_PK_2, "native", 1_000_000, &policy,
2093            );
2094            assert!(result.is_err());
2095            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
2096        }
2097
2098        #[test]
2099        fn test_classic_asset_payment() {
2100            let usdc_asset = format!("USDC:{}", TEST_PK);
2101            let payment_op = Operation {
2102                source_account: None,
2103                body: OperationBody::Payment(soroban_rs::xdr::PaymentOp {
2104                    destination: create_muxed_account(TEST_PK_2),
2105                    asset: XdrAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2106                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2107                        issuer: create_account_id(TEST_PK),
2108                    }),
2109                    amount: 1_000_000,
2110                }),
2111            };
2112
2113            let tx = Transaction {
2114                source_account: create_muxed_account(TEST_PK),
2115                fee: 100,
2116                seq_num: SequenceNumber(1),
2117                cond: soroban_rs::xdr::Preconditions::None,
2118                memo: soroban_rs::xdr::Memo::None,
2119                operations: vec![payment_op].try_into().unwrap(),
2120                ext: TransactionExt::V0,
2121            };
2122
2123            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2124                tx,
2125                signatures: vec![].try_into().unwrap(),
2126            });
2127
2128            let mut policy = RelayerStellarPolicy::default();
2129            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2130                asset: usdc_asset.clone(),
2131                metadata: None,
2132                swap_config: None,
2133                max_allowed_fee: None,
2134            }]);
2135
2136            let result = StellarTransactionValidator::validate_token_payment(
2137                &envelope,
2138                TEST_PK_2,
2139                &usdc_asset,
2140                1_000_000,
2141                &policy,
2142            );
2143            assert!(result.is_ok());
2144        }
2145
2146        #[test]
2147        fn test_multiple_payments_finds_correct_token() {
2148            // Create envelope with two payments: one USDC to relayer, one XLM to someone else
2149            let usdc_asset = format!("USDC:{}", TEST_PK);
2150            let usdc_payment = Operation {
2151                source_account: None,
2152                body: OperationBody::Payment(soroban_rs::xdr::PaymentOp {
2153                    destination: create_muxed_account(TEST_PK_2),
2154                    asset: XdrAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2155                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2156                        issuer: create_account_id(TEST_PK),
2157                    }),
2158                    amount: 500_000,
2159                }),
2160            };
2161
2162            let xlm_payment = create_native_payment_operation(TEST_PK, 1_000_000);
2163
2164            let tx = Transaction {
2165                source_account: create_muxed_account(TEST_PK),
2166                fee: 100,
2167                seq_num: SequenceNumber(1),
2168                cond: soroban_rs::xdr::Preconditions::None,
2169                memo: soroban_rs::xdr::Memo::None,
2170                operations: vec![xlm_payment, usdc_payment].try_into().unwrap(),
2171                ext: TransactionExt::V0,
2172            };
2173
2174            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2175                tx,
2176                signatures: vec![].try_into().unwrap(),
2177            });
2178
2179            let policy = RelayerStellarPolicy::default();
2180
2181            // Should find the USDC payment to TEST_PK_2
2182            let result = StellarTransactionValidator::validate_token_payment(
2183                &envelope,
2184                TEST_PK_2,
2185                &usdc_asset,
2186                500_000,
2187                &policy,
2188            );
2189            assert!(result.is_ok());
2190        }
2191    }
2192
2193    mod validate_user_fee_payment_amounts_tests {
2194        use super::*;
2195        use soroban_rs::stellar_rpc_client::{
2196            GetLatestLedgerResponse, SimulateTransactionResponse,
2197        };
2198        use soroban_rs::xdr::WriteXdr;
2199
2200        const USDC_ISSUER: &str = TEST_PK;
2201
2202        fn create_usdc_payment_envelope(
2203            source: &str,
2204            destination: &str,
2205            amount: i64,
2206        ) -> TransactionEnvelope {
2207            let payment_op = Operation {
2208                source_account: None,
2209                body: OperationBody::Payment(PaymentOp {
2210                    destination: create_muxed_account(destination),
2211                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2212                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2213                        issuer: create_account_id(USDC_ISSUER),
2214                    }),
2215                    amount,
2216                }),
2217            };
2218
2219            let tx = Transaction {
2220                source_account: create_muxed_account(source),
2221                fee: 100,
2222                seq_num: SequenceNumber(1),
2223                cond: soroban_rs::xdr::Preconditions::None,
2224                memo: soroban_rs::xdr::Memo::None,
2225                operations: vec![payment_op].try_into().unwrap(),
2226                ext: TransactionExt::V0,
2227            };
2228
2229            TransactionEnvelope::Tx(TransactionV1Envelope {
2230                tx,
2231                signatures: vec![].try_into().unwrap(),
2232            })
2233        }
2234
2235        fn create_usdc_policy() -> RelayerStellarPolicy {
2236            let usdc_asset = format!("USDC:{}", USDC_ISSUER);
2237            let mut policy = RelayerStellarPolicy::default();
2238            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2239                asset: usdc_asset,
2240                metadata: None,
2241                swap_config: None,
2242                max_allowed_fee: None,
2243            }]);
2244            policy
2245        }
2246
2247        fn create_mock_provider_with_balance(balance: i64) -> MockStellarProviderTrait {
2248            let mut provider = MockStellarProviderTrait::new();
2249
2250            // Mock get_account for source account
2251            provider.expect_get_account().returning(move |_| {
2252                Box::pin(ready(Ok(AccountEntry {
2253                    account_id: create_account_id(TEST_PK),
2254                    balance,
2255                    seq_num: SequenceNumber(1),
2256                    num_sub_entries: 0,
2257                    inflation_dest: None,
2258                    flags: 0,
2259                    home_domain: Default::default(),
2260                    thresholds: Thresholds([0; 4]),
2261                    signers: Default::default(),
2262                    ext: AccountEntryExt::V0,
2263                })))
2264            });
2265
2266            // Mock get_latest_ledger for fee estimation
2267            provider.expect_get_latest_ledger().returning(|| {
2268                Box::pin(ready(Ok(GetLatestLedgerResponse {
2269                    id: "test".to_string(),
2270                    protocol_version: 20,
2271                    sequence: 1000,
2272                })))
2273            });
2274
2275            // Mock simulate_transaction_envelope for Soroban fee estimation
2276            provider
2277                .expect_simulate_transaction_envelope()
2278                .returning(|_| {
2279                    Box::pin(ready(Ok(SimulateTransactionResponse {
2280                        min_resource_fee: 100,
2281                        transaction_data: String::new(),
2282                        ..Default::default()
2283                    })))
2284                });
2285
2286            // Mock get_ledger_entries for trustline balance check
2287            provider.expect_get_ledger_entries().returning(|_| {
2288                use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
2289                use soroban_rs::xdr::{
2290                    LedgerEntry, LedgerEntryData, LedgerEntryExt, TrustLineAsset, TrustLineEntry,
2291                    TrustLineEntryExt,
2292                };
2293
2294                let trustline_entry = TrustLineEntry {
2295                    account_id: create_account_id(TEST_PK),
2296                    asset: TrustLineAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2297                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2298                        issuer: create_account_id(TEST_PK_2),
2299                    }),
2300                    balance: 10_000_000, // 10 USDC
2301                    limit: 1_000_000_000,
2302                    flags: 1,
2303                    ext: TrustLineEntryExt::V0,
2304                };
2305
2306                let ledger_entry = LedgerEntry {
2307                    last_modified_ledger_seq: 0,
2308                    data: LedgerEntryData::Trustline(trustline_entry),
2309                    ext: LedgerEntryExt::V0,
2310                };
2311
2312                let xdr_base64 = ledger_entry
2313                    .data
2314                    .to_xdr_base64(soroban_rs::xdr::Limits::none())
2315                    .unwrap();
2316
2317                Box::pin(ready(Ok(GetLedgerEntriesResponse {
2318                    entries: Some(vec![LedgerEntryResult {
2319                        key: String::new(),
2320                        xdr: xdr_base64,
2321                        last_modified_ledger: 0,
2322                        live_until_ledger_seq_ledger_seq: None,
2323                    }]),
2324                    latest_ledger: 0,
2325                })))
2326            });
2327
2328            provider
2329        }
2330
2331        fn create_mock_dex_service() -> MockStellarDexServiceTrait {
2332            let mut dex_service = MockStellarDexServiceTrait::new();
2333            dex_service
2334                .expect_get_xlm_to_token_quote()
2335                .returning(|_, _, _, _| {
2336                    Box::pin(ready(Ok(
2337                        crate::services::stellar_dex::StellarQuoteResponse {
2338                            input_asset: "native".to_string(),
2339                            output_asset: format!("USDC:{}", USDC_ISSUER),
2340                            in_amount: 100,
2341                            out_amount: 1_000_000, // 0.1 USDC
2342                            price_impact_pct: 0.0,
2343                            slippage_bps: 100,
2344                            path: None,
2345                        },
2346                    )))
2347                });
2348            dex_service
2349        }
2350
2351        #[tokio::test]
2352        async fn test_valid_fee_payment() {
2353            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2354            let policy = create_usdc_policy();
2355            let provider = create_mock_provider_with_balance(10_000_000_000);
2356            let dex_service = create_mock_dex_service();
2357
2358            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2359                &envelope,
2360                TEST_PK_2,
2361                &policy,
2362                &provider,
2363                &dex_service,
2364            )
2365            .await;
2366
2367            assert!(result.is_ok());
2368        }
2369
2370        #[tokio::test]
2371        async fn test_no_fee_payment() {
2372            // Envelope with payment to different address (not the relayer)
2373            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK, 1_000_000);
2374            let policy = create_usdc_policy();
2375            let provider = create_mock_provider_with_balance(10_000_000_000);
2376            let dex_service = create_mock_dex_service();
2377
2378            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2379                &envelope,
2380                TEST_PK_2, // Different from destination
2381                &policy,
2382                &provider,
2383                &dex_service,
2384            )
2385            .await;
2386
2387            assert!(result.is_err());
2388            assert!(result
2389                .unwrap_err()
2390                .to_string()
2391                .contains("must include a fee payment operation to the relayer"));
2392        }
2393
2394        #[tokio::test]
2395        async fn test_multiple_fee_payments_rejected() {
2396            // Create envelope with two USDC payments to relayer
2397            let payment1 = Operation {
2398                source_account: None,
2399                body: OperationBody::Payment(PaymentOp {
2400                    destination: create_muxed_account(TEST_PK_2),
2401                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2402                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2403                        issuer: create_account_id(USDC_ISSUER),
2404                    }),
2405                    amount: 500_000,
2406                }),
2407            };
2408            let payment2 = Operation {
2409                source_account: None,
2410                body: OperationBody::Payment(PaymentOp {
2411                    destination: create_muxed_account(TEST_PK_2),
2412                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2413                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2414                        issuer: create_account_id(USDC_ISSUER),
2415                    }),
2416                    amount: 500_000,
2417                }),
2418            };
2419
2420            let tx = Transaction {
2421                source_account: create_muxed_account(TEST_PK),
2422                fee: 100,
2423                seq_num: SequenceNumber(1),
2424                cond: soroban_rs::xdr::Preconditions::None,
2425                memo: soroban_rs::xdr::Memo::None,
2426                operations: vec![payment1, payment2].try_into().unwrap(),
2427                ext: TransactionExt::V0,
2428            };
2429
2430            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2431                tx,
2432                signatures: vec![].try_into().unwrap(),
2433            });
2434
2435            let policy = create_usdc_policy();
2436            let provider = create_mock_provider_with_balance(10_000_000_000);
2437            let dex_service = create_mock_dex_service();
2438
2439            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2440                &envelope,
2441                TEST_PK_2,
2442                &policy,
2443                &provider,
2444                &dex_service,
2445            )
2446            .await;
2447
2448            assert!(result.is_err());
2449            assert!(result
2450                .unwrap_err()
2451                .to_string()
2452                .contains("exactly one fee payment operation"));
2453        }
2454
2455        #[tokio::test]
2456        async fn test_token_not_allowed() {
2457            // Create envelope with EURC payment (not in allowed list)
2458            let payment_op = Operation {
2459                source_account: None,
2460                body: OperationBody::Payment(PaymentOp {
2461                    destination: create_muxed_account(TEST_PK_2),
2462                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2463                        asset_code: soroban_rs::xdr::AssetCode4(*b"EURC"),
2464                        issuer: create_account_id(TEST_PK),
2465                    }),
2466                    amount: 1_000_000,
2467                }),
2468            };
2469
2470            let tx = Transaction {
2471                source_account: create_muxed_account(TEST_PK),
2472                fee: 100,
2473                seq_num: SequenceNumber(1),
2474                cond: soroban_rs::xdr::Preconditions::None,
2475                memo: soroban_rs::xdr::Memo::None,
2476                operations: vec![payment_op].try_into().unwrap(),
2477                ext: TransactionExt::V0,
2478            };
2479
2480            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2481                tx,
2482                signatures: vec![].try_into().unwrap(),
2483            });
2484
2485            let policy = create_usdc_policy(); // Only USDC is allowed
2486
2487            let provider = create_mock_provider_with_balance(10_000_000_000);
2488            let dex_service = create_mock_dex_service();
2489
2490            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2491                &envelope,
2492                TEST_PK_2,
2493                &policy,
2494                &provider,
2495                &dex_service,
2496            )
2497            .await;
2498
2499            assert!(result.is_err());
2500            assert!(result
2501                .unwrap_err()
2502                .to_string()
2503                .contains("not in allowed tokens list"));
2504        }
2505
2506        #[tokio::test]
2507        async fn test_fee_exceeds_token_max() {
2508            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2509            let usdc_asset = format!("USDC:{}", USDC_ISSUER);
2510            let mut policy = RelayerStellarPolicy::default();
2511            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2512                asset: usdc_asset,
2513                metadata: None,
2514                swap_config: None,
2515                max_allowed_fee: Some(500_000), // Lower than payment amount
2516            }]);
2517
2518            let provider = create_mock_provider_with_balance(10_000_000_000);
2519            let dex_service = create_mock_dex_service();
2520
2521            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2522                &envelope,
2523                TEST_PK_2,
2524                &policy,
2525                &provider,
2526                &dex_service,
2527            )
2528            .await;
2529
2530            assert!(result.is_err());
2531            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
2532        }
2533
2534        #[tokio::test]
2535        async fn test_insufficient_payment_amount() {
2536            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2537            let policy = create_usdc_policy();
2538            let provider = create_mock_provider_with_balance(10_000_000_000);
2539
2540            // Mock DEX to require more than the payment amount
2541            let mut dex_service = MockStellarDexServiceTrait::new();
2542            dex_service
2543                .expect_get_xlm_to_token_quote()
2544                .returning(|_, _, _, _| {
2545                    Box::pin(ready(Ok(
2546                        crate::services::stellar_dex::StellarQuoteResponse {
2547                            input_asset: "native".to_string(),
2548                            output_asset: "USDC:...".to_string(),
2549                            in_amount: 200,
2550                            out_amount: 2_000_000, // More than the 1M payment
2551                            price_impact_pct: 0.0,
2552                            slippage_bps: 100,
2553                            path: None,
2554                        },
2555                    )))
2556                });
2557
2558            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2559                &envelope,
2560                TEST_PK_2,
2561                &policy,
2562                &provider,
2563                &dex_service,
2564            )
2565            .await;
2566
2567            assert!(result.is_err());
2568            assert!(result
2569                .unwrap_err()
2570                .to_string()
2571                .contains("Insufficient token payment"));
2572        }
2573
2574        #[tokio::test]
2575        async fn test_insufficient_user_balance() {
2576            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2577            let policy = create_usdc_policy();
2578
2579            // Create provider with low USDC trustline balance
2580            let mut provider = MockStellarProviderTrait::new();
2581
2582            provider.expect_get_account().returning(move |_| {
2583                Box::pin(ready(Ok(AccountEntry {
2584                    account_id: create_account_id(TEST_PK),
2585                    balance: 10_000_000_000,
2586                    seq_num: SequenceNumber(1),
2587                    num_sub_entries: 0,
2588                    inflation_dest: None,
2589                    flags: 0,
2590                    home_domain: Default::default(),
2591                    thresholds: Thresholds([0; 4]),
2592                    signers: Default::default(),
2593                    ext: AccountEntryExt::V0,
2594                })))
2595            });
2596
2597            provider.expect_get_latest_ledger().returning(|| {
2598                Box::pin(ready(Ok(GetLatestLedgerResponse {
2599                    id: "test".to_string(),
2600                    protocol_version: 20,
2601                    sequence: 1000,
2602                })))
2603            });
2604
2605            provider
2606                .expect_simulate_transaction_envelope()
2607                .returning(|_| {
2608                    Box::pin(ready(Ok(SimulateTransactionResponse {
2609                        min_resource_fee: 100,
2610                        transaction_data: String::new(),
2611                        ..Default::default()
2612                    })))
2613                });
2614
2615            // Mock get_ledger_entries with low USDC balance
2616            provider.expect_get_ledger_entries().returning(|_| {
2617                use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
2618                use soroban_rs::xdr::{
2619                    LedgerEntry, LedgerEntryData, LedgerEntryExt, TrustLineAsset, TrustLineEntry,
2620                    TrustLineEntryExt,
2621                };
2622
2623                let trustline_entry = TrustLineEntry {
2624                    account_id: create_account_id(TEST_PK),
2625                    asset: TrustLineAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2626                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2627                        issuer: create_account_id(USDC_ISSUER),
2628                    }),
2629                    balance: 500_000, // Only 0.05 USDC - insufficient
2630                    limit: 1_000_000_000,
2631                    flags: 1,
2632                    ext: TrustLineEntryExt::V0,
2633                };
2634
2635                let ledger_entry = LedgerEntry {
2636                    last_modified_ledger_seq: 0,
2637                    data: LedgerEntryData::Trustline(trustline_entry),
2638                    ext: LedgerEntryExt::V0,
2639                };
2640
2641                let xdr_base64 = ledger_entry
2642                    .data
2643                    .to_xdr_base64(soroban_rs::xdr::Limits::none())
2644                    .unwrap();
2645
2646                Box::pin(ready(Ok(GetLedgerEntriesResponse {
2647                    entries: Some(vec![LedgerEntryResult {
2648                        key: String::new(),
2649                        xdr: xdr_base64,
2650                        last_modified_ledger: 0,
2651                        live_until_ledger_seq_ledger_seq: None,
2652                    }]),
2653                    latest_ledger: 0,
2654                })))
2655            });
2656
2657            let dex_service = create_mock_dex_service();
2658
2659            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2660                &envelope,
2661                TEST_PK_2,
2662                &policy,
2663                &provider,
2664                &dex_service,
2665            )
2666            .await;
2667
2668            assert!(result.is_err());
2669            assert!(result
2670                .unwrap_err()
2671                .to_string()
2672                .contains("Insufficient balance"));
2673        }
2674
2675        #[tokio::test]
2676        async fn test_valid_fee_payment_with_usdc() {
2677            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2678            let policy = create_usdc_policy();
2679            let provider = create_mock_provider_with_balance(10_000_000_000);
2680            let dex_service = create_mock_dex_service();
2681
2682            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2683                &envelope,
2684                TEST_PK_2,
2685                &policy,
2686                &provider,
2687                &dex_service,
2688            )
2689            .await;
2690
2691            assert!(result.is_ok());
2692        }
2693
2694        #[tokio::test]
2695        async fn test_dex_conversion_failure() {
2696            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2697            let policy = create_usdc_policy();
2698            let provider = create_mock_provider_with_balance(10_000_000_000);
2699
2700            let mut dex_service = MockStellarDexServiceTrait::new();
2701            dex_service
2702                .expect_get_xlm_to_token_quote()
2703                .returning(|_, _, _, _| {
2704                    Box::pin(ready(Err(
2705                        crate::services::stellar_dex::StellarDexServiceError::UnknownError(
2706                            "DEX unavailable".to_string(),
2707                        ),
2708                    )))
2709                });
2710
2711            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2712                &envelope,
2713                TEST_PK_2,
2714                &policy,
2715                &provider,
2716                &dex_service,
2717            )
2718            .await;
2719
2720            assert!(result.is_err());
2721            assert!(result
2722                .unwrap_err()
2723                .to_string()
2724                .contains("Failed to convert XLM fee to token"));
2725        }
2726    }
2727
2728    mod validate_contract_invocation_tests {
2729        use super::*;
2730
2731        #[test]
2732        fn test_invoke_contract_allowed() {
2733            let invoke_op = InvokeHostFunctionOp {
2734                host_function: HostFunction::InvokeContract(InvokeContractArgs {
2735                    contract_address: ScAddress::Contract(soroban_rs::xdr::ContractId(
2736                        soroban_rs::xdr::Hash([0u8; 32]),
2737                    )),
2738                    function_name: ScSymbol("test".try_into().unwrap()),
2739                    args: Default::default(),
2740                }),
2741                auth: Default::default(),
2742            };
2743
2744            let policy = RelayerStellarPolicy::default();
2745            assert!(StellarTransactionValidator::validate_contract_invocation(
2746                &invoke_op, 0, TEST_PK_2, &policy
2747            )
2748            .is_ok());
2749        }
2750
2751        #[test]
2752        fn test_create_contract_rejected() {
2753            let invoke_op = InvokeHostFunctionOp {
2754                host_function: HostFunction::CreateContract(soroban_rs::xdr::CreateContractArgs {
2755                    contract_id_preimage: soroban_rs::xdr::ContractIdPreimage::Address(
2756                        soroban_rs::xdr::ContractIdPreimageFromAddress {
2757                            address: ScAddress::Account(create_account_id(TEST_PK)),
2758                            salt: soroban_rs::xdr::Uint256([0u8; 32]),
2759                        },
2760                    ),
2761                    executable: soroban_rs::xdr::ContractExecutable::Wasm(soroban_rs::xdr::Hash(
2762                        [0u8; 32],
2763                    )),
2764                }),
2765                auth: Default::default(),
2766            };
2767
2768            let policy = RelayerStellarPolicy::default();
2769            let result = StellarTransactionValidator::validate_contract_invocation(
2770                &invoke_op, 0, TEST_PK_2, &policy,
2771            );
2772            assert!(result.is_err());
2773            assert!(result
2774                .unwrap_err()
2775                .to_string()
2776                .contains("CreateContract not allowed"));
2777        }
2778
2779        #[test]
2780        fn test_upload_wasm_rejected() {
2781            let invoke_op = InvokeHostFunctionOp {
2782                host_function: HostFunction::UploadContractWasm(vec![0u8; 100].try_into().unwrap()),
2783                auth: Default::default(),
2784            };
2785
2786            let policy = RelayerStellarPolicy::default();
2787            let result = StellarTransactionValidator::validate_contract_invocation(
2788                &invoke_op, 0, TEST_PK_2, &policy,
2789            );
2790            assert!(result.is_err());
2791            assert!(result
2792                .unwrap_err()
2793                .to_string()
2794                .contains("UploadContractWasm not allowed"));
2795        }
2796
2797        #[test]
2798        fn test_relayer_in_auth_rejected() {
2799            let auth_entry = SorobanAuthorizationEntry {
2800                credentials: SorobanCredentials::Address(
2801                    soroban_rs::xdr::SorobanAddressCredentials {
2802                        address: ScAddress::Account(create_account_id(TEST_PK_2)),
2803                        nonce: 0,
2804                        signature_expiration_ledger: 0,
2805                        signature: soroban_rs::xdr::ScVal::Void,
2806                    },
2807                ),
2808                root_invocation: soroban_rs::xdr::SorobanAuthorizedInvocation {
2809                    function: SorobanAuthorizedFunction::ContractFn(
2810                        soroban_rs::xdr::InvokeContractArgs {
2811                            contract_address: ScAddress::Contract(soroban_rs::xdr::ContractId(
2812                                soroban_rs::xdr::Hash([0u8; 32]),
2813                            )),
2814                            function_name: ScSymbol("test".try_into().unwrap()),
2815                            args: Default::default(),
2816                        },
2817                    ),
2818                    sub_invocations: Default::default(),
2819                },
2820            };
2821
2822            let invoke_op = InvokeHostFunctionOp {
2823                host_function: HostFunction::InvokeContract(InvokeContractArgs {
2824                    contract_address: ScAddress::Contract(soroban_rs::xdr::ContractId(
2825                        soroban_rs::xdr::Hash([0u8; 32]),
2826                    )),
2827                    function_name: ScSymbol("test".try_into().unwrap()),
2828                    args: Default::default(),
2829                }),
2830                auth: vec![auth_entry].try_into().unwrap(),
2831            };
2832
2833            let policy = RelayerStellarPolicy::default();
2834            let result = StellarTransactionValidator::validate_contract_invocation(
2835                &invoke_op, 0, TEST_PK_2, // Relayer address matches auth entry
2836                &policy,
2837            );
2838            assert!(result.is_err());
2839            assert!(result.unwrap_err().to_string().contains("requires relayer"));
2840        }
2841    }
2842}