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            if now > max_time {
815                return Err(StellarTransactionValidationError::ValidationError(format!(
816                    "Transaction has expired: max_time={max_time}, current_time={now}"
817                )));
818            }
819
820            // Check if transaction is not yet valid (optional check, but good to have)
821            if min_time > 0 && now < min_time {
822                return Err(StellarTransactionValidationError::ValidationError(format!(
823                    "Transaction is not yet valid: min_time={min_time}, current_time={now}"
824                )));
825            }
826        }
827        // If no time bounds are set, we don't fail here (some transactions may not have them)
828        // The caller can decide if time bounds are required
829
830        Ok(())
831    }
832
833    /// Validate that transaction validity duration is within the maximum allowed time
834    ///
835    /// This prevents price fluctuations and protects the relayer from losses.
836    /// The transaction must have time bounds set and the validity duration must not exceed
837    /// the maximum allowed duration.
838    ///
839    /// # Arguments
840    /// * `envelope` - The transaction envelope to validate
841    /// * `max_duration` - Maximum allowed validity duration
842    ///
843    /// # Returns
844    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
845    pub fn validate_transaction_validity_duration(
846        envelope: &TransactionEnvelope,
847        max_duration: Duration,
848    ) -> Result<(), StellarTransactionValidationError> {
849        let time_bounds = extract_time_bounds(envelope);
850
851        if let Some(bounds) = time_bounds {
852            let max_time =
853                DateTime::from_timestamp(bounds.max_time.0 as i64, 0).ok_or_else(|| {
854                    StellarTransactionValidationError::ValidationError(
855                        "Invalid max_time in time bounds".to_string(),
856                    )
857                })?;
858            let now = Utc::now();
859            let duration = max_time - now;
860
861            if duration > max_duration {
862                return Err(StellarTransactionValidationError::ValidationError(format!(
863                    "Transaction validity duration ({duration:?}) exceeds maximum allowed duration ({max_duration:?})"
864                )));
865            }
866        } else {
867            return Err(StellarTransactionValidationError::ValidationError(
868                "Transaction must have time bounds set".to_string(),
869            ));
870        }
871
872        Ok(())
873    }
874
875    /// Comprehensive validation for user fee payment transactions
876    ///
877    /// This function performs all validations required for user-paid fee transactions.
878    /// It validates:
879    /// 1. Transaction structure and operations (via gasless_transaction_validation)
880    /// 2. Fee payment operations exist and are valid
881    /// 3. Allowed token validation
882    /// 4. Token max fee validation
883    /// 5. Payment amount is sufficient (compares with required fee including margin)
884    /// 6. Transaction validity duration (if max_validity_duration is provided)
885    ///
886    /// This function is used by both fee-bump and sign-transaction flows.
887    /// For sign-transaction flows, pass `max_validity_duration` to enforce time bounds.
888    /// For fee-bump flows, pass `None` as transactions may not have time bounds set yet.
889    ///
890    /// # Arguments
891    /// * `envelope` - The transaction envelope to validate
892    /// * `relayer_address` - The relayer's Stellar address
893    /// * `policy` - The relayer policy containing fee payment strategy and token settings
894    /// * `provider` - Provider for Stellar RPC operations
895    /// * `dex_service` - DEX service for fetching quotes to validate payment amounts
896    /// * `max_validity_duration` - Optional maximum allowed transaction validity duration.
897    ///   If provided, validates that the transaction's time bounds don't exceed this duration.
898    ///   This protects against price fluctuations for user-paid fee transactions when signing.
899    ///   Pass `None` for fee-bump flows where time bounds may not be set yet.
900    ///
901    /// # Returns
902    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
903    pub async fn validate_user_fee_payment_transaction<P, D>(
904        envelope: &TransactionEnvelope,
905        relayer_address: &str,
906        policy: &RelayerStellarPolicy,
907        provider: &P,
908        dex_service: &D,
909        max_validity_duration: Option<Duration>,
910    ) -> Result<(), StellarTransactionValidationError>
911    where
912        P: StellarProviderTrait + Send + Sync,
913        D: StellarDexServiceTrait + Send + Sync,
914    {
915        // Step 1: Comprehensive security validation for gasless transactions
916        // Include duration validation if max_validity_duration is provided
917        Self::gasless_transaction_validation(
918            envelope,
919            relayer_address,
920            policy,
921            provider,
922            max_validity_duration,
923        )
924        .await?;
925
926        // Step 2: Validate fee payment amounts
927        Self::validate_user_fee_payment_amounts(
928            envelope,
929            relayer_address,
930            policy,
931            provider,
932            dex_service,
933        )
934        .await?;
935
936        Ok(())
937    }
938
939    /// Validate fee payment amounts for user-paid fee transactions
940    ///
941    /// This function validates that the fee payment operation exists, is valid,
942    /// and the payment amount is sufficient. It's separated from the core validation
943    /// to allow reuse in different flows.
944    ///
945    /// # Arguments
946    /// * `envelope` - The transaction envelope to validate
947    /// * `relayer_address` - The relayer's Stellar address
948    /// * `policy` - The relayer policy containing fee payment strategy and token settings
949    /// * `provider` - Provider for Stellar RPC operations
950    /// * `dex_service` - DEX service for fetching quotes to validate payment amounts
951    ///
952    /// # Returns
953    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
954    async fn validate_user_fee_payment_amounts<P, D>(
955        envelope: &TransactionEnvelope,
956        relayer_address: &str,
957        policy: &RelayerStellarPolicy,
958        provider: &P,
959        dex_service: &D,
960    ) -> Result<(), StellarTransactionValidationError>
961    where
962        P: StellarProviderTrait + Send + Sync,
963        D: StellarDexServiceTrait + Send + Sync,
964    {
965        // Extract the fee payment for amount validation
966        let payments = Self::extract_relayer_payments(envelope, relayer_address)?;
967        if payments.is_empty() {
968            return Err(StellarTransactionValidationError::ValidationError(
969                "Gasless transactions must include a fee payment operation to the relayer"
970                    .to_string(),
971            ));
972        }
973
974        // Validate only one fee payment operation
975        if payments.len() > 1 {
976            return Err(StellarTransactionValidationError::ValidationError(format!(
977                "Gasless transactions must include exactly one fee payment operation to the relayer, found {}",
978                payments.len()
979            )));
980        }
981
982        // Extract the single payment
983        let (asset_id, amount) = &payments[0];
984
985        // Validate fee payment token
986        Self::validate_allowed_token(asset_id, policy)?;
987
988        // Validate max fee
989        Self::validate_token_max_fee(asset_id, *amount, policy)?;
990
991        // Calculate required XLM fee using estimate_fee (handles Soroban transactions correctly)
992
993        let mut required_xlm_fee = estimate_fee(envelope, provider, None).await.map_err(|e| {
994            StellarTransactionValidationError::ValidationError(format!(
995                "Failed to estimate fee: {e}",
996            ))
997        })?;
998
999        let is_soroban = xdr_needs_simulation(envelope).unwrap_or(false);
1000        if !is_soroban {
1001            // For regular transactions, fee-bump needs base fee (100 stroops)
1002            required_xlm_fee += STELLAR_DEFAULT_TRANSACTION_FEE as u64;
1003        }
1004
1005        let fee_quote = convert_xlm_fee_to_token(dex_service, policy, required_xlm_fee, asset_id)
1006            .await
1007            .map_err(|e| {
1008                StellarTransactionValidationError::ValidationError(format!(
1009                    "Failed to convert XLM fee to token {asset_id}: {e}",
1010                ))
1011            })?;
1012
1013        // Compare payment amount with required token amount (from convert_xlm_fee_to_token which includes margin)
1014        if *amount < fee_quote.fee_in_token {
1015            return Err(StellarTransactionValidationError::InsufficientTokenPayment(
1016                fee_quote.fee_in_token,
1017                *amount,
1018            ));
1019        }
1020
1021        // Validate user token balance
1022        Self::validate_user_token_balance(envelope, asset_id, fee_quote.fee_in_token, provider)
1023            .await?;
1024
1025        Ok(())
1026    }
1027
1028    /// Validate that user has sufficient token balance to pay the transaction fee
1029    ///
1030    /// This function checks that the user's account has enough balance of the specified
1031    /// fee token to cover the required transaction fee. This prevents users from getting
1032    /// quotes or building transactions they cannot afford.
1033    ///
1034    /// # Arguments
1035    /// * `envelope` - The transaction envelope to extract source account from
1036    /// * `fee_token` - The token identifier (e.g., "native" or "USDC:GA5Z...")
1037    /// * `required_fee_amount` - The required fee amount in token's smallest unit (stroops)
1038    /// * `provider` - Provider for Stellar RPC operations to fetch balance
1039    ///
1040    /// # Returns
1041    /// Ok(()) if validation passes, StellarTransactionValidationError if validation fails
1042    pub async fn validate_user_token_balance<P>(
1043        envelope: &TransactionEnvelope,
1044        fee_token: &str,
1045        required_fee_amount: u64,
1046        provider: &P,
1047    ) -> Result<(), StellarTransactionValidationError>
1048    where
1049        P: StellarProviderTrait + Send + Sync,
1050    {
1051        // Extract source account from envelope
1052        let source_account = extract_source_account(envelope).map_err(|e| {
1053            StellarTransactionValidationError::ValidationError(format!(
1054                "Failed to extract source account: {e}"
1055            ))
1056        })?;
1057
1058        // Fetch user's token balance
1059        let user_balance = get_token_balance(provider, &source_account, fee_token)
1060            .await
1061            .map_err(|e| {
1062                StellarTransactionValidationError::ValidationError(format!(
1063                    "Failed to fetch user balance for token {fee_token}: {e}",
1064                ))
1065            })?;
1066
1067        // Check if balance is sufficient
1068        if user_balance < required_fee_amount {
1069            return Err(StellarTransactionValidationError::ValidationError(format!(
1070                "Insufficient balance: user has {user_balance} {fee_token} but needs {required_fee_amount} {fee_token} for transaction fee"
1071            )));
1072        }
1073
1074        Ok(())
1075    }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081    use crate::domain::transaction::stellar::test_helpers::{
1082        create_account_id, create_muxed_account, create_native_payment_operation,
1083        create_simple_v1_envelope, TEST_CONTRACT, TEST_PK, TEST_PK_2,
1084    };
1085    use crate::models::{AssetSpec, StellarAllowedTokensPolicy};
1086    use crate::services::provider::MockStellarProviderTrait;
1087    use crate::services::stellar_dex::MockStellarDexServiceTrait;
1088    use futures::future::ready;
1089    use soroban_rs::xdr::{
1090        AccountEntry, AccountEntryExt, Asset as XdrAsset, ChangeTrustAsset, ChangeTrustOp,
1091        HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Operation, OperationBody,
1092        ScAddress, ScSymbol, SequenceNumber, SorobanAuthorizationEntry, SorobanAuthorizedFunction,
1093        SorobanCredentials, Thresholds, TimeBounds, TimePoint, Transaction, TransactionEnvelope,
1094        TransactionExt, TransactionV1Envelope,
1095    };
1096
1097    #[test]
1098    fn test_empty_operations_rejected() {
1099        let result = validate_operations(&[]);
1100        assert!(result.is_err());
1101        assert!(result
1102            .unwrap_err()
1103            .to_string()
1104            .contains("at least one operation"));
1105    }
1106
1107    #[test]
1108    fn test_too_many_operations_rejected() {
1109        let ops = vec![
1110            OperationSpec::Payment {
1111                destination: TEST_PK.to_string(),
1112                amount: 1000,
1113                asset: AssetSpec::Native,
1114            };
1115            101
1116        ];
1117        let result = validate_operations(&ops);
1118        assert!(result.is_err());
1119        assert!(result
1120            .unwrap_err()
1121            .to_string()
1122            .contains("maximum allowed is 100"));
1123    }
1124
1125    #[test]
1126    fn test_soroban_exclusivity_enforced() {
1127        // Multiple Soroban operations should fail
1128        let ops = vec![
1129            OperationSpec::InvokeContract {
1130                contract_address: TEST_CONTRACT.to_string(),
1131                function_name: "test".to_string(),
1132                args: vec![],
1133                auth: None,
1134            },
1135            OperationSpec::CreateContract {
1136                source: crate::models::ContractSource::Address {
1137                    address: TEST_PK.to_string(),
1138                },
1139                wasm_hash: "abc123".to_string(),
1140                salt: None,
1141                constructor_args: None,
1142                auth: None,
1143            },
1144        ];
1145        let result = validate_operations(&ops);
1146        assert!(result.is_err());
1147
1148        // Soroban mixed with non-Soroban should fail
1149        let ops = vec![
1150            OperationSpec::InvokeContract {
1151                contract_address: TEST_CONTRACT.to_string(),
1152                function_name: "test".to_string(),
1153                args: vec![],
1154                auth: None,
1155            },
1156            OperationSpec::Payment {
1157                destination: TEST_PK.to_string(),
1158                amount: 1000,
1159                asset: AssetSpec::Native,
1160            },
1161        ];
1162        let result = validate_operations(&ops);
1163        assert!(result.is_err());
1164        assert!(result
1165            .unwrap_err()
1166            .to_string()
1167            .contains("Soroban operations must be exclusive"));
1168    }
1169
1170    #[test]
1171    fn test_soroban_memo_restriction() {
1172        let soroban_op = vec![OperationSpec::InvokeContract {
1173            contract_address: TEST_CONTRACT.to_string(),
1174            function_name: "test".to_string(),
1175            args: vec![],
1176            auth: None,
1177        }];
1178
1179        // Soroban with text memo should fail
1180        let result = validate_soroban_memo_restriction(
1181            &soroban_op,
1182            &Some(MemoSpec::Text {
1183                value: "test".to_string(),
1184            }),
1185        );
1186        assert!(result.is_err());
1187
1188        // Soroban with MemoNone should succeed
1189        let result = validate_soroban_memo_restriction(&soroban_op, &Some(MemoSpec::None));
1190        assert!(result.is_ok());
1191
1192        // Soroban with no memo should succeed
1193        let result = validate_soroban_memo_restriction(&soroban_op, &None);
1194        assert!(result.is_ok());
1195    }
1196
1197    mod validate_fee_token_structure_tests {
1198        use super::*;
1199
1200        #[test]
1201        fn test_native_xlm_valid() {
1202            assert!(StellarTransactionValidator::validate_fee_token_structure("native").is_ok());
1203            assert!(StellarTransactionValidator::validate_fee_token_structure("XLM").is_ok());
1204            assert!(StellarTransactionValidator::validate_fee_token_structure("").is_ok());
1205        }
1206
1207        #[test]
1208        fn test_contract_address_valid() {
1209            assert!(
1210                StellarTransactionValidator::validate_fee_token_structure(TEST_CONTRACT).is_ok()
1211            );
1212        }
1213
1214        #[test]
1215        fn test_contract_address_invalid_length() {
1216            let result = StellarTransactionValidator::validate_fee_token_structure("C123");
1217            assert!(result.is_err());
1218            assert!(result
1219                .unwrap_err()
1220                .to_string()
1221                .contains("Invalid fee_token format"));
1222        }
1223
1224        #[test]
1225        fn test_classic_asset_valid() {
1226            let result = StellarTransactionValidator::validate_fee_token_structure(&format!(
1227                "USDC:{}",
1228                TEST_PK
1229            ));
1230            assert!(result.is_ok());
1231        }
1232
1233        #[test]
1234        fn test_classic_asset_code_too_long() {
1235            let result = StellarTransactionValidator::validate_fee_token_structure(&format!(
1236                "VERYLONGCODE1:{}",
1237                TEST_PK
1238            ));
1239            assert!(result.is_err());
1240            assert!(result
1241                .unwrap_err()
1242                .to_string()
1243                .contains("Invalid asset code length"));
1244        }
1245
1246        #[test]
1247        fn test_classic_asset_invalid_issuer_length() {
1248            let result = StellarTransactionValidator::validate_fee_token_structure("USDC:GSHORT");
1249            assert!(result.is_err());
1250            assert!(result
1251                .unwrap_err()
1252                .to_string()
1253                .contains("Invalid issuer address length"));
1254        }
1255
1256        #[test]
1257        fn test_classic_asset_invalid_issuer_prefix() {
1258            let result = StellarTransactionValidator::validate_fee_token_structure(
1259                "USDC:SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
1260            );
1261            assert!(result.is_err());
1262            assert!(result
1263                .unwrap_err()
1264                .to_string()
1265                .contains("Invalid issuer address prefix"));
1266        }
1267
1268        #[test]
1269        fn test_invalid_format_multiple_colons() {
1270            let result =
1271                StellarTransactionValidator::validate_fee_token_structure("USDC:ISSUER:EXTRA");
1272            assert!(result.is_err());
1273            assert!(result
1274                .unwrap_err()
1275                .to_string()
1276                .contains("Invalid fee_token format"));
1277        }
1278    }
1279
1280    mod validate_allowed_token_tests {
1281        use super::*;
1282
1283        #[test]
1284        fn test_empty_allowed_list_allows_all() {
1285            let policy = RelayerStellarPolicy::default();
1286            assert!(StellarTransactionValidator::validate_allowed_token("native", &policy).is_ok());
1287            assert!(
1288                StellarTransactionValidator::validate_allowed_token(TEST_CONTRACT, &policy).is_ok()
1289            );
1290        }
1291
1292        #[test]
1293        fn test_native_allowed() {
1294            let mut policy = RelayerStellarPolicy::default();
1295            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1296                asset: "native".to_string(),
1297                metadata: None,
1298                swap_config: None,
1299                max_allowed_fee: None,
1300            }]);
1301            assert!(StellarTransactionValidator::validate_allowed_token("native", &policy).is_ok());
1302            assert!(StellarTransactionValidator::validate_allowed_token("", &policy).is_ok());
1303        }
1304
1305        #[test]
1306        fn test_native_not_allowed() {
1307            let mut policy = RelayerStellarPolicy::default();
1308            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1309                asset: format!("USDC:{}", TEST_PK),
1310                metadata: None,
1311                swap_config: None,
1312                max_allowed_fee: None,
1313            }]);
1314            let result = StellarTransactionValidator::validate_allowed_token("native", &policy);
1315            assert!(result.is_err());
1316            assert!(result
1317                .unwrap_err()
1318                .to_string()
1319                .contains("Native XLM not in allowed tokens list"));
1320        }
1321
1322        #[test]
1323        fn test_token_allowed() {
1324            let token = format!("USDC:{}", TEST_PK);
1325            let mut policy = RelayerStellarPolicy::default();
1326            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1327                asset: token.clone(),
1328                metadata: None,
1329                swap_config: None,
1330                max_allowed_fee: None,
1331            }]);
1332            assert!(StellarTransactionValidator::validate_allowed_token(&token, &policy).is_ok());
1333        }
1334
1335        #[test]
1336        fn test_token_not_allowed() {
1337            let mut policy = RelayerStellarPolicy::default();
1338            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1339                asset: format!("USDC:{}", TEST_PK),
1340                metadata: None,
1341                swap_config: None,
1342                max_allowed_fee: None,
1343            }]);
1344            let result = StellarTransactionValidator::validate_allowed_token(
1345                &format!("AQUA:{}", TEST_PK_2),
1346                &policy,
1347            );
1348            assert!(result.is_err());
1349            assert!(result
1350                .unwrap_err()
1351                .to_string()
1352                .contains("not in allowed tokens list"));
1353        }
1354    }
1355
1356    mod validate_max_fee_tests {
1357        use super::*;
1358
1359        #[test]
1360        fn test_no_max_fee_allows_any() {
1361            let policy = RelayerStellarPolicy::default();
1362            assert!(StellarTransactionValidator::validate_max_fee(1_000_000, &policy).is_ok());
1363        }
1364
1365        #[test]
1366        fn test_fee_within_limit() {
1367            let mut policy = RelayerStellarPolicy::default();
1368            policy.max_fee = Some(1_000_000);
1369            assert!(StellarTransactionValidator::validate_max_fee(500_000, &policy).is_ok());
1370        }
1371
1372        #[test]
1373        fn test_fee_exceeds_limit() {
1374            let mut policy = RelayerStellarPolicy::default();
1375            policy.max_fee = Some(1_000_000);
1376            let result = StellarTransactionValidator::validate_max_fee(2_000_000, &policy);
1377            assert!(result.is_err());
1378            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
1379        }
1380    }
1381
1382    mod validate_token_max_fee_tests {
1383        use super::*;
1384
1385        #[test]
1386        fn test_no_token_entry() {
1387            let policy = RelayerStellarPolicy::default();
1388            assert!(StellarTransactionValidator::validate_token_max_fee(
1389                "USDC:ISSUER",
1390                1_000_000,
1391                &policy
1392            )
1393            .is_ok());
1394        }
1395
1396        #[test]
1397        fn test_no_max_allowed_fee_in_entry() {
1398            let mut policy = RelayerStellarPolicy::default();
1399            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1400                asset: "USDC:ISSUER".to_string(),
1401                metadata: None,
1402                swap_config: None,
1403                max_allowed_fee: None,
1404            }]);
1405            assert!(StellarTransactionValidator::validate_token_max_fee(
1406                "USDC:ISSUER",
1407                1_000_000,
1408                &policy
1409            )
1410            .is_ok());
1411        }
1412
1413        #[test]
1414        fn test_fee_within_token_limit() {
1415            let mut policy = RelayerStellarPolicy::default();
1416            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1417                asset: "USDC:ISSUER".to_string(),
1418                metadata: None,
1419                swap_config: None,
1420                max_allowed_fee: Some(1_000_000),
1421            }]);
1422            assert!(StellarTransactionValidator::validate_token_max_fee(
1423                "USDC:ISSUER",
1424                500_000,
1425                &policy
1426            )
1427            .is_ok());
1428        }
1429
1430        #[test]
1431        fn test_fee_exceeds_token_limit() {
1432            let mut policy = RelayerStellarPolicy::default();
1433            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
1434                asset: "USDC:ISSUER".to_string(),
1435                metadata: None,
1436                swap_config: None,
1437                max_allowed_fee: Some(1_000_000),
1438            }]);
1439            let result = StellarTransactionValidator::validate_token_max_fee(
1440                "USDC:ISSUER",
1441                2_000_000,
1442                &policy,
1443            );
1444            assert!(result.is_err());
1445            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
1446        }
1447    }
1448
1449    mod extract_relayer_payments_tests {
1450        use super::*;
1451
1452        #[test]
1453        fn test_extract_single_payment() {
1454            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1455            let payments =
1456                StellarTransactionValidator::extract_relayer_payments(&envelope, TEST_PK_2)
1457                    .unwrap();
1458            assert_eq!(payments.len(), 1);
1459            assert_eq!(payments[0].0, "native");
1460            assert_eq!(payments[0].1, 1_000_000);
1461        }
1462
1463        #[test]
1464        fn test_extract_no_payments_to_relayer() {
1465            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1466            let payments =
1467                StellarTransactionValidator::extract_relayer_payments(&envelope, TEST_PK).unwrap();
1468            assert_eq!(payments.len(), 0);
1469        }
1470
1471        #[test]
1472        fn test_extract_negative_amount_rejected() {
1473            let payment_op = Operation {
1474                source_account: None,
1475                body: OperationBody::Payment(soroban_rs::xdr::PaymentOp {
1476                    destination: create_muxed_account(TEST_PK_2),
1477                    asset: XdrAsset::Native,
1478                    amount: -100, // Negative amount
1479                }),
1480            };
1481
1482            let tx = Transaction {
1483                source_account: create_muxed_account(TEST_PK),
1484                fee: 100,
1485                seq_num: SequenceNumber(1),
1486                cond: soroban_rs::xdr::Preconditions::None,
1487                memo: soroban_rs::xdr::Memo::None,
1488                operations: vec![payment_op].try_into().unwrap(),
1489                ext: TransactionExt::V0,
1490            };
1491
1492            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1493                tx,
1494                signatures: vec![].try_into().unwrap(),
1495            });
1496
1497            let result =
1498                StellarTransactionValidator::extract_relayer_payments(&envelope, TEST_PK_2);
1499            assert!(result.is_err());
1500            assert!(result
1501                .unwrap_err()
1502                .to_string()
1503                .contains("Negative payment amount"));
1504        }
1505    }
1506
1507    mod validate_time_bounds_tests {
1508        use super::*;
1509
1510        #[test]
1511        fn test_no_time_bounds_is_ok() {
1512            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1513            assert!(
1514                StellarTransactionValidator::validate_time_bounds_not_expired(&envelope).is_ok()
1515            );
1516        }
1517
1518        #[test]
1519        fn test_valid_time_bounds() {
1520            let now = Utc::now().timestamp() as u64;
1521            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1522
1523            let tx = Transaction {
1524                source_account: create_muxed_account(TEST_PK),
1525                fee: 100,
1526                seq_num: SequenceNumber(1),
1527                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1528                    min_time: TimePoint(now - 60),
1529                    max_time: TimePoint(now + 60),
1530                }),
1531                memo: soroban_rs::xdr::Memo::None,
1532                operations: vec![payment_op].try_into().unwrap(),
1533                ext: TransactionExt::V0,
1534            };
1535
1536            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1537                tx,
1538                signatures: vec![].try_into().unwrap(),
1539            });
1540
1541            assert!(
1542                StellarTransactionValidator::validate_time_bounds_not_expired(&envelope).is_ok()
1543            );
1544        }
1545
1546        #[test]
1547        fn test_expired_transaction() {
1548            let now = Utc::now().timestamp() as u64;
1549            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1550
1551            let tx = Transaction {
1552                source_account: create_muxed_account(TEST_PK),
1553                fee: 100,
1554                seq_num: SequenceNumber(1),
1555                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1556                    min_time: TimePoint(now - 120),
1557                    max_time: TimePoint(now - 60), // Expired
1558                }),
1559                memo: soroban_rs::xdr::Memo::None,
1560                operations: vec![payment_op].try_into().unwrap(),
1561                ext: TransactionExt::V0,
1562            };
1563
1564            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1565                tx,
1566                signatures: vec![].try_into().unwrap(),
1567            });
1568
1569            let result = StellarTransactionValidator::validate_time_bounds_not_expired(&envelope);
1570            assert!(result.is_err());
1571            assert!(result.unwrap_err().to_string().contains("has expired"));
1572        }
1573
1574        #[test]
1575        fn test_not_yet_valid_transaction() {
1576            let now = Utc::now().timestamp() as u64;
1577            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1578
1579            let tx = Transaction {
1580                source_account: create_muxed_account(TEST_PK),
1581                fee: 100,
1582                seq_num: SequenceNumber(1),
1583                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1584                    min_time: TimePoint(now + 60), // Not yet valid
1585                    max_time: TimePoint(now + 120),
1586                }),
1587                memo: soroban_rs::xdr::Memo::None,
1588                operations: vec![payment_op].try_into().unwrap(),
1589                ext: TransactionExt::V0,
1590            };
1591
1592            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1593                tx,
1594                signatures: vec![].try_into().unwrap(),
1595            });
1596
1597            let result = StellarTransactionValidator::validate_time_bounds_not_expired(&envelope);
1598            assert!(result.is_err());
1599            assert!(result.unwrap_err().to_string().contains("not yet valid"));
1600        }
1601    }
1602
1603    mod validate_transaction_validity_duration_tests {
1604        use super::*;
1605
1606        #[test]
1607        fn test_duration_within_limit() {
1608            let now = Utc::now().timestamp() as u64;
1609            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1610
1611            let tx = Transaction {
1612                source_account: create_muxed_account(TEST_PK),
1613                fee: 100,
1614                seq_num: SequenceNumber(1),
1615                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1616                    min_time: TimePoint(0),
1617                    max_time: TimePoint(now + 60), // 1 minute from now
1618                }),
1619                memo: soroban_rs::xdr::Memo::None,
1620                operations: vec![payment_op].try_into().unwrap(),
1621                ext: TransactionExt::V0,
1622            };
1623
1624            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1625                tx,
1626                signatures: vec![].try_into().unwrap(),
1627            });
1628
1629            let max_duration = Duration::minutes(5);
1630            assert!(
1631                StellarTransactionValidator::validate_transaction_validity_duration(
1632                    &envelope,
1633                    max_duration
1634                )
1635                .is_ok()
1636            );
1637        }
1638
1639        #[test]
1640        fn test_duration_exceeds_limit() {
1641            let now = Utc::now().timestamp() as u64;
1642            let payment_op = create_native_payment_operation(TEST_PK_2, 1_000_000);
1643
1644            let tx = Transaction {
1645                source_account: create_muxed_account(TEST_PK),
1646                fee: 100,
1647                seq_num: SequenceNumber(1),
1648                cond: soroban_rs::xdr::Preconditions::Time(TimeBounds {
1649                    min_time: TimePoint(0),
1650                    max_time: TimePoint(now + 600), // 10 minutes from now
1651                }),
1652                memo: soroban_rs::xdr::Memo::None,
1653                operations: vec![payment_op].try_into().unwrap(),
1654                ext: TransactionExt::V0,
1655            };
1656
1657            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1658                tx,
1659                signatures: vec![].try_into().unwrap(),
1660            });
1661
1662            let max_duration = Duration::minutes(5);
1663            let result = StellarTransactionValidator::validate_transaction_validity_duration(
1664                &envelope,
1665                max_duration,
1666            );
1667            assert!(result.is_err());
1668            assert!(result
1669                .unwrap_err()
1670                .to_string()
1671                .contains("exceeds maximum allowed duration"));
1672        }
1673
1674        #[test]
1675        fn test_no_time_bounds_rejected() {
1676            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1677            let max_duration = Duration::minutes(5);
1678            let result = StellarTransactionValidator::validate_transaction_validity_duration(
1679                &envelope,
1680                max_duration,
1681            );
1682            assert!(result.is_err());
1683            assert!(result
1684                .unwrap_err()
1685                .to_string()
1686                .contains("must have time bounds set"));
1687        }
1688    }
1689
1690    mod validate_sequence_number_tests {
1691        use super::*;
1692
1693        #[tokio::test]
1694        async fn test_valid_sequence_number() {
1695            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1696
1697            let mut provider = MockStellarProviderTrait::new();
1698            provider.expect_get_account().returning(|_| {
1699                Box::pin(ready(Ok(AccountEntry {
1700                    account_id: create_account_id(TEST_PK),
1701                    balance: 1_000_000_000,
1702                    seq_num: SequenceNumber(0), // Current sequence is 0, tx sequence is 1
1703                    num_sub_entries: 0,
1704                    inflation_dest: None,
1705                    flags: 0,
1706                    home_domain: Default::default(),
1707                    thresholds: Thresholds([0; 4]),
1708                    signers: Default::default(),
1709                    ext: AccountEntryExt::V0,
1710                })))
1711            });
1712
1713            assert!(
1714                StellarTransactionValidator::validate_sequence_number(&envelope, &provider)
1715                    .await
1716                    .is_ok()
1717            );
1718        }
1719
1720        #[tokio::test]
1721        async fn test_equal_sequence_rejected() {
1722            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1723
1724            let mut provider = MockStellarProviderTrait::new();
1725            provider.expect_get_account().returning(|_| {
1726                Box::pin(ready(Ok(AccountEntry {
1727                    account_id: create_account_id(TEST_PK),
1728                    balance: 1_000_000_000,
1729                    seq_num: SequenceNumber(1), // Same as tx sequence
1730                    num_sub_entries: 0,
1731                    inflation_dest: None,
1732                    flags: 0,
1733                    home_domain: Default::default(),
1734                    thresholds: Thresholds([0; 4]),
1735                    signers: Default::default(),
1736                    ext: AccountEntryExt::V0,
1737                })))
1738            });
1739
1740            let result =
1741                StellarTransactionValidator::validate_sequence_number(&envelope, &provider).await;
1742            assert!(result.is_err());
1743            assert!(result
1744                .unwrap_err()
1745                .to_string()
1746                .contains("strictly greater than"));
1747        }
1748
1749        #[tokio::test]
1750        async fn test_past_sequence_rejected() {
1751            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1752
1753            let mut provider = MockStellarProviderTrait::new();
1754            provider.expect_get_account().returning(|_| {
1755                Box::pin(ready(Ok(AccountEntry {
1756                    account_id: create_account_id(TEST_PK),
1757                    balance: 1_000_000_000,
1758                    seq_num: SequenceNumber(10), // Higher than tx sequence
1759                    num_sub_entries: 0,
1760                    inflation_dest: None,
1761                    flags: 0,
1762                    home_domain: Default::default(),
1763                    thresholds: Thresholds([0; 4]),
1764                    signers: Default::default(),
1765                    ext: AccountEntryExt::V0,
1766                })))
1767            });
1768
1769            let result =
1770                StellarTransactionValidator::validate_sequence_number(&envelope, &provider).await;
1771            assert!(result.is_err());
1772            assert!(result.unwrap_err().to_string().contains("is invalid"));
1773        }
1774    }
1775
1776    mod validate_operations_count_tests {
1777        use super::*;
1778
1779        #[test]
1780        fn test_valid_operations_count() {
1781            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1782            assert!(StellarTransactionValidator::validate_operations_count(&envelope).is_ok());
1783        }
1784
1785        #[test]
1786        fn test_too_many_operations() {
1787            // VecM has a max of 100, so we can't actually create an envelope with 101 operations
1788            // Instead, we test that the validation logic works correctly by checking the limit
1789            // This test verifies the validation function would reject if it could receive such an envelope
1790
1791            // Create an envelope with exactly 100 operations (the maximum)
1792            let operations: Vec<Operation> = (0..100)
1793                .map(|_| create_native_payment_operation(TEST_PK_2, 100))
1794                .collect();
1795
1796            let tx = Transaction {
1797                source_account: create_muxed_account(TEST_PK),
1798                fee: 100,
1799                seq_num: SequenceNumber(1),
1800                cond: soroban_rs::xdr::Preconditions::None,
1801                memo: soroban_rs::xdr::Memo::None,
1802                operations: operations.try_into().unwrap(),
1803                ext: TransactionExt::V0,
1804            };
1805
1806            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1807                tx,
1808                signatures: vec![].try_into().unwrap(),
1809            });
1810
1811            // 100 operations should be OK
1812            let result = StellarTransactionValidator::validate_operations_count(&envelope);
1813            assert!(result.is_ok());
1814        }
1815    }
1816
1817    mod validate_source_account_tests {
1818        use super::*;
1819
1820        #[test]
1821        fn test_source_account_not_relayer() {
1822            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1823            assert!(
1824                StellarTransactionValidator::validate_source_account_not_relayer(
1825                    &envelope, TEST_PK_2
1826                )
1827                .is_ok()
1828            );
1829        }
1830
1831        #[test]
1832        fn test_source_account_is_relayer_rejected() {
1833            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1834            let result = StellarTransactionValidator::validate_source_account_not_relayer(
1835                &envelope, TEST_PK,
1836            );
1837            assert!(result.is_err());
1838            assert!(result
1839                .unwrap_err()
1840                .to_string()
1841                .contains("cannot be the relayer address"));
1842        }
1843    }
1844
1845    mod validate_operation_types_tests {
1846        use super::*;
1847
1848        #[test]
1849        fn test_payment_operation_allowed() {
1850            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1851            let policy = RelayerStellarPolicy::default();
1852            assert!(StellarTransactionValidator::validate_operation_types(
1853                &envelope, TEST_PK_2, &policy
1854            )
1855            .is_ok());
1856        }
1857
1858        #[test]
1859        fn test_account_merge_rejected() {
1860            let operation = Operation {
1861                source_account: None,
1862                body: OperationBody::AccountMerge(create_muxed_account(TEST_PK_2)),
1863            };
1864
1865            let tx = Transaction {
1866                source_account: create_muxed_account(TEST_PK),
1867                fee: 100,
1868                seq_num: SequenceNumber(1),
1869                cond: soroban_rs::xdr::Preconditions::None,
1870                memo: soroban_rs::xdr::Memo::None,
1871                operations: vec![operation].try_into().unwrap(),
1872                ext: TransactionExt::V0,
1873            };
1874
1875            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1876                tx,
1877                signatures: vec![].try_into().unwrap(),
1878            });
1879
1880            let policy = RelayerStellarPolicy::default();
1881            let result = StellarTransactionValidator::validate_operation_types(
1882                &envelope, TEST_PK_2, &policy,
1883            );
1884            assert!(result.is_err());
1885            assert!(result
1886                .unwrap_err()
1887                .to_string()
1888                .contains("AccountMerge operations are not allowed"));
1889        }
1890
1891        #[test]
1892        fn test_set_options_rejected() {
1893            let operation = Operation {
1894                source_account: None,
1895                body: OperationBody::SetOptions(soroban_rs::xdr::SetOptionsOp {
1896                    inflation_dest: None,
1897                    clear_flags: None,
1898                    set_flags: None,
1899                    master_weight: None,
1900                    low_threshold: None,
1901                    med_threshold: None,
1902                    high_threshold: None,
1903                    home_domain: None,
1904                    signer: None,
1905                }),
1906            };
1907
1908            let tx = Transaction {
1909                source_account: create_muxed_account(TEST_PK),
1910                fee: 100,
1911                seq_num: SequenceNumber(1),
1912                cond: soroban_rs::xdr::Preconditions::None,
1913                memo: soroban_rs::xdr::Memo::None,
1914                operations: vec![operation].try_into().unwrap(),
1915                ext: TransactionExt::V0,
1916            };
1917
1918            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1919                tx,
1920                signatures: vec![].try_into().unwrap(),
1921            });
1922
1923            let policy = RelayerStellarPolicy::default();
1924            let result = StellarTransactionValidator::validate_operation_types(
1925                &envelope, TEST_PK_2, &policy,
1926            );
1927            assert!(result.is_err());
1928            assert!(result
1929                .unwrap_err()
1930                .to_string()
1931                .contains("SetOptions operations are not allowed"));
1932        }
1933
1934        #[test]
1935        fn test_change_trust_allowed() {
1936            let operation = Operation {
1937                source_account: None,
1938                body: OperationBody::ChangeTrust(ChangeTrustOp {
1939                    line: ChangeTrustAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
1940                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
1941                        issuer: create_account_id(TEST_PK_2),
1942                    }),
1943                    limit: 1_000_000_000,
1944                }),
1945            };
1946
1947            let tx = Transaction {
1948                source_account: create_muxed_account(TEST_PK),
1949                fee: 100,
1950                seq_num: SequenceNumber(1),
1951                cond: soroban_rs::xdr::Preconditions::None,
1952                memo: soroban_rs::xdr::Memo::None,
1953                operations: vec![operation].try_into().unwrap(),
1954                ext: TransactionExt::V0,
1955            };
1956
1957            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
1958                tx,
1959                signatures: vec![].try_into().unwrap(),
1960            });
1961
1962            let policy = RelayerStellarPolicy::default();
1963            assert!(StellarTransactionValidator::validate_operation_types(
1964                &envelope, TEST_PK_2, &policy
1965            )
1966            .is_ok());
1967        }
1968    }
1969
1970    mod validate_token_payment_tests {
1971        use super::*;
1972
1973        #[test]
1974        fn test_valid_native_payment() {
1975            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1976            let policy = RelayerStellarPolicy::default();
1977
1978            let result = StellarTransactionValidator::validate_token_payment(
1979                &envelope, TEST_PK_2, "native", 1_000_000, &policy,
1980            );
1981            assert!(result.is_ok());
1982        }
1983
1984        #[test]
1985        fn test_no_payment_to_relayer() {
1986            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
1987            let policy = RelayerStellarPolicy::default();
1988
1989            // Wrong relayer address - no payments will match
1990            let result = StellarTransactionValidator::validate_token_payment(
1991                &envelope, TEST_PK, // Different from destination
1992                "native", 1_000_000, &policy,
1993            );
1994            assert!(result.is_err());
1995            assert!(result
1996                .unwrap_err()
1997                .to_string()
1998                .contains("No payment operation found to relayer"));
1999        }
2000
2001        #[test]
2002        fn test_wrong_token_in_payment() {
2003            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2004            let policy = RelayerStellarPolicy::default();
2005
2006            // Expecting USDC but envelope has native payment
2007            let result = StellarTransactionValidator::validate_token_payment(
2008                &envelope,
2009                TEST_PK_2,
2010                &format!("USDC:{}", TEST_PK),
2011                1_000_000,
2012                &policy,
2013            );
2014            assert!(result.is_err());
2015            assert!(result
2016                .unwrap_err()
2017                .to_string()
2018                .contains("No payment found for expected token"));
2019        }
2020
2021        #[test]
2022        fn test_insufficient_payment_amount() {
2023            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2024            let policy = RelayerStellarPolicy::default();
2025
2026            // Expecting 2M but envelope has 1M payment
2027            let result = StellarTransactionValidator::validate_token_payment(
2028                &envelope, TEST_PK_2, "native", 2_000_000, &policy,
2029            );
2030            assert!(result.is_err());
2031            assert!(result
2032                .unwrap_err()
2033                .to_string()
2034                .contains("Insufficient token payment"));
2035        }
2036
2037        #[test]
2038        fn test_payment_within_tolerance() {
2039            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2040            let policy = RelayerStellarPolicy::default();
2041
2042            let result = StellarTransactionValidator::validate_token_payment(
2043                &envelope, TEST_PK_2, "native", 990_000, &policy,
2044            );
2045            assert!(result.is_ok());
2046        }
2047
2048        #[test]
2049        fn test_token_not_in_allowed_list() {
2050            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2051            let mut policy = RelayerStellarPolicy::default();
2052            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2053                asset: format!("USDC:{}", TEST_PK),
2054                metadata: None,
2055                swap_config: None,
2056                max_allowed_fee: None,
2057            }]);
2058
2059            // Native payment but only USDC is allowed
2060            let result = StellarTransactionValidator::validate_token_payment(
2061                &envelope, TEST_PK_2, "native", 1_000_000, &policy,
2062            );
2063            assert!(result.is_err());
2064            assert!(result
2065                .unwrap_err()
2066                .to_string()
2067                .contains("not in allowed tokens list"));
2068        }
2069
2070        #[test]
2071        fn test_payment_exceeds_token_max_fee() {
2072            let envelope = create_simple_v1_envelope(TEST_PK, TEST_PK_2);
2073            let mut policy = RelayerStellarPolicy::default();
2074            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2075                asset: "native".to_string(),
2076                metadata: None,
2077                swap_config: None,
2078                max_allowed_fee: Some(500_000), // Max 0.5 XLM
2079            }]);
2080
2081            // Payment is 1M but max allowed is 500K
2082            let result = StellarTransactionValidator::validate_token_payment(
2083                &envelope, TEST_PK_2, "native", 1_000_000, &policy,
2084            );
2085            assert!(result.is_err());
2086            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
2087        }
2088
2089        #[test]
2090        fn test_classic_asset_payment() {
2091            let usdc_asset = format!("USDC:{}", TEST_PK);
2092            let payment_op = Operation {
2093                source_account: None,
2094                body: OperationBody::Payment(soroban_rs::xdr::PaymentOp {
2095                    destination: create_muxed_account(TEST_PK_2),
2096                    asset: XdrAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2097                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2098                        issuer: create_account_id(TEST_PK),
2099                    }),
2100                    amount: 1_000_000,
2101                }),
2102            };
2103
2104            let tx = Transaction {
2105                source_account: create_muxed_account(TEST_PK),
2106                fee: 100,
2107                seq_num: SequenceNumber(1),
2108                cond: soroban_rs::xdr::Preconditions::None,
2109                memo: soroban_rs::xdr::Memo::None,
2110                operations: vec![payment_op].try_into().unwrap(),
2111                ext: TransactionExt::V0,
2112            };
2113
2114            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2115                tx,
2116                signatures: vec![].try_into().unwrap(),
2117            });
2118
2119            let mut policy = RelayerStellarPolicy::default();
2120            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2121                asset: usdc_asset.clone(),
2122                metadata: None,
2123                swap_config: None,
2124                max_allowed_fee: None,
2125            }]);
2126
2127            let result = StellarTransactionValidator::validate_token_payment(
2128                &envelope,
2129                TEST_PK_2,
2130                &usdc_asset,
2131                1_000_000,
2132                &policy,
2133            );
2134            assert!(result.is_ok());
2135        }
2136
2137        #[test]
2138        fn test_multiple_payments_finds_correct_token() {
2139            // Create envelope with two payments: one USDC to relayer, one XLM to someone else
2140            let usdc_asset = format!("USDC:{}", TEST_PK);
2141            let usdc_payment = Operation {
2142                source_account: None,
2143                body: OperationBody::Payment(soroban_rs::xdr::PaymentOp {
2144                    destination: create_muxed_account(TEST_PK_2),
2145                    asset: XdrAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2146                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2147                        issuer: create_account_id(TEST_PK),
2148                    }),
2149                    amount: 500_000,
2150                }),
2151            };
2152
2153            let xlm_payment = create_native_payment_operation(TEST_PK, 1_000_000);
2154
2155            let tx = Transaction {
2156                source_account: create_muxed_account(TEST_PK),
2157                fee: 100,
2158                seq_num: SequenceNumber(1),
2159                cond: soroban_rs::xdr::Preconditions::None,
2160                memo: soroban_rs::xdr::Memo::None,
2161                operations: vec![xlm_payment, usdc_payment].try_into().unwrap(),
2162                ext: TransactionExt::V0,
2163            };
2164
2165            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2166                tx,
2167                signatures: vec![].try_into().unwrap(),
2168            });
2169
2170            let policy = RelayerStellarPolicy::default();
2171
2172            // Should find the USDC payment to TEST_PK_2
2173            let result = StellarTransactionValidator::validate_token_payment(
2174                &envelope,
2175                TEST_PK_2,
2176                &usdc_asset,
2177                500_000,
2178                &policy,
2179            );
2180            assert!(result.is_ok());
2181        }
2182    }
2183
2184    mod validate_user_fee_payment_amounts_tests {
2185        use super::*;
2186        use soroban_rs::stellar_rpc_client::{
2187            GetLatestLedgerResponse, SimulateTransactionResponse,
2188        };
2189        use soroban_rs::xdr::WriteXdr;
2190
2191        const USDC_ISSUER: &str = TEST_PK;
2192
2193        fn create_usdc_payment_envelope(
2194            source: &str,
2195            destination: &str,
2196            amount: i64,
2197        ) -> TransactionEnvelope {
2198            let payment_op = Operation {
2199                source_account: None,
2200                body: OperationBody::Payment(PaymentOp {
2201                    destination: create_muxed_account(destination),
2202                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2203                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2204                        issuer: create_account_id(USDC_ISSUER),
2205                    }),
2206                    amount,
2207                }),
2208            };
2209
2210            let tx = Transaction {
2211                source_account: create_muxed_account(source),
2212                fee: 100,
2213                seq_num: SequenceNumber(1),
2214                cond: soroban_rs::xdr::Preconditions::None,
2215                memo: soroban_rs::xdr::Memo::None,
2216                operations: vec![payment_op].try_into().unwrap(),
2217                ext: TransactionExt::V0,
2218            };
2219
2220            TransactionEnvelope::Tx(TransactionV1Envelope {
2221                tx,
2222                signatures: vec![].try_into().unwrap(),
2223            })
2224        }
2225
2226        fn create_usdc_policy() -> RelayerStellarPolicy {
2227            let usdc_asset = format!("USDC:{}", USDC_ISSUER);
2228            let mut policy = RelayerStellarPolicy::default();
2229            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2230                asset: usdc_asset,
2231                metadata: None,
2232                swap_config: None,
2233                max_allowed_fee: None,
2234            }]);
2235            policy
2236        }
2237
2238        fn create_mock_provider_with_balance(balance: i64) -> MockStellarProviderTrait {
2239            let mut provider = MockStellarProviderTrait::new();
2240
2241            // Mock get_account for source account
2242            provider.expect_get_account().returning(move |_| {
2243                Box::pin(ready(Ok(AccountEntry {
2244                    account_id: create_account_id(TEST_PK),
2245                    balance,
2246                    seq_num: SequenceNumber(1),
2247                    num_sub_entries: 0,
2248                    inflation_dest: None,
2249                    flags: 0,
2250                    home_domain: Default::default(),
2251                    thresholds: Thresholds([0; 4]),
2252                    signers: Default::default(),
2253                    ext: AccountEntryExt::V0,
2254                })))
2255            });
2256
2257            // Mock get_latest_ledger for fee estimation
2258            provider.expect_get_latest_ledger().returning(|| {
2259                Box::pin(ready(Ok(GetLatestLedgerResponse {
2260                    id: "test".to_string(),
2261                    protocol_version: 20,
2262                    sequence: 1000,
2263                })))
2264            });
2265
2266            // Mock simulate_transaction_envelope for Soroban fee estimation
2267            provider
2268                .expect_simulate_transaction_envelope()
2269                .returning(|_| {
2270                    Box::pin(ready(Ok(SimulateTransactionResponse {
2271                        min_resource_fee: 100,
2272                        transaction_data: String::new(),
2273                        ..Default::default()
2274                    })))
2275                });
2276
2277            // Mock get_ledger_entries for trustline balance check
2278            provider.expect_get_ledger_entries().returning(|_| {
2279                use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
2280                use soroban_rs::xdr::{
2281                    LedgerEntry, LedgerEntryData, LedgerEntryExt, TrustLineAsset, TrustLineEntry,
2282                    TrustLineEntryExt,
2283                };
2284
2285                let trustline_entry = TrustLineEntry {
2286                    account_id: create_account_id(TEST_PK),
2287                    asset: TrustLineAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2288                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2289                        issuer: create_account_id(TEST_PK_2),
2290                    }),
2291                    balance: 10_000_000, // 10 USDC
2292                    limit: 1_000_000_000,
2293                    flags: 1,
2294                    ext: TrustLineEntryExt::V0,
2295                };
2296
2297                let ledger_entry = LedgerEntry {
2298                    last_modified_ledger_seq: 0,
2299                    data: LedgerEntryData::Trustline(trustline_entry),
2300                    ext: LedgerEntryExt::V0,
2301                };
2302
2303                let xdr_base64 = ledger_entry
2304                    .data
2305                    .to_xdr_base64(soroban_rs::xdr::Limits::none())
2306                    .unwrap();
2307
2308                Box::pin(ready(Ok(GetLedgerEntriesResponse {
2309                    entries: Some(vec![LedgerEntryResult {
2310                        key: String::new(),
2311                        xdr: xdr_base64,
2312                        last_modified_ledger: 0,
2313                        live_until_ledger_seq_ledger_seq: None,
2314                    }]),
2315                    latest_ledger: 0,
2316                })))
2317            });
2318
2319            provider
2320        }
2321
2322        fn create_mock_dex_service() -> MockStellarDexServiceTrait {
2323            let mut dex_service = MockStellarDexServiceTrait::new();
2324            dex_service
2325                .expect_get_xlm_to_token_quote()
2326                .returning(|_, _, _, _| {
2327                    Box::pin(ready(Ok(
2328                        crate::services::stellar_dex::StellarQuoteResponse {
2329                            input_asset: "native".to_string(),
2330                            output_asset: format!("USDC:{}", USDC_ISSUER),
2331                            in_amount: 100,
2332                            out_amount: 1_000_000, // 0.1 USDC
2333                            price_impact_pct: 0.0,
2334                            slippage_bps: 100,
2335                            path: None,
2336                        },
2337                    )))
2338                });
2339            dex_service
2340        }
2341
2342        #[tokio::test]
2343        async fn test_valid_fee_payment() {
2344            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2345            let policy = create_usdc_policy();
2346            let provider = create_mock_provider_with_balance(10_000_000_000);
2347            let dex_service = create_mock_dex_service();
2348
2349            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2350                &envelope,
2351                TEST_PK_2,
2352                &policy,
2353                &provider,
2354                &dex_service,
2355            )
2356            .await;
2357
2358            assert!(result.is_ok());
2359        }
2360
2361        #[tokio::test]
2362        async fn test_no_fee_payment() {
2363            // Envelope with payment to different address (not the relayer)
2364            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK, 1_000_000);
2365            let policy = create_usdc_policy();
2366            let provider = create_mock_provider_with_balance(10_000_000_000);
2367            let dex_service = create_mock_dex_service();
2368
2369            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2370                &envelope,
2371                TEST_PK_2, // Different from destination
2372                &policy,
2373                &provider,
2374                &dex_service,
2375            )
2376            .await;
2377
2378            assert!(result.is_err());
2379            assert!(result
2380                .unwrap_err()
2381                .to_string()
2382                .contains("must include a fee payment operation to the relayer"));
2383        }
2384
2385        #[tokio::test]
2386        async fn test_multiple_fee_payments_rejected() {
2387            // Create envelope with two USDC payments to relayer
2388            let payment1 = Operation {
2389                source_account: None,
2390                body: OperationBody::Payment(PaymentOp {
2391                    destination: create_muxed_account(TEST_PK_2),
2392                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2393                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2394                        issuer: create_account_id(USDC_ISSUER),
2395                    }),
2396                    amount: 500_000,
2397                }),
2398            };
2399            let payment2 = Operation {
2400                source_account: None,
2401                body: OperationBody::Payment(PaymentOp {
2402                    destination: create_muxed_account(TEST_PK_2),
2403                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2404                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2405                        issuer: create_account_id(USDC_ISSUER),
2406                    }),
2407                    amount: 500_000,
2408                }),
2409            };
2410
2411            let tx = Transaction {
2412                source_account: create_muxed_account(TEST_PK),
2413                fee: 100,
2414                seq_num: SequenceNumber(1),
2415                cond: soroban_rs::xdr::Preconditions::None,
2416                memo: soroban_rs::xdr::Memo::None,
2417                operations: vec![payment1, payment2].try_into().unwrap(),
2418                ext: TransactionExt::V0,
2419            };
2420
2421            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2422                tx,
2423                signatures: vec![].try_into().unwrap(),
2424            });
2425
2426            let policy = create_usdc_policy();
2427            let provider = create_mock_provider_with_balance(10_000_000_000);
2428            let dex_service = create_mock_dex_service();
2429
2430            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2431                &envelope,
2432                TEST_PK_2,
2433                &policy,
2434                &provider,
2435                &dex_service,
2436            )
2437            .await;
2438
2439            assert!(result.is_err());
2440            assert!(result
2441                .unwrap_err()
2442                .to_string()
2443                .contains("exactly one fee payment operation"));
2444        }
2445
2446        #[tokio::test]
2447        async fn test_token_not_allowed() {
2448            // Create envelope with EURC payment (not in allowed list)
2449            let payment_op = Operation {
2450                source_account: None,
2451                body: OperationBody::Payment(PaymentOp {
2452                    destination: create_muxed_account(TEST_PK_2),
2453                    asset: soroban_rs::xdr::Asset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2454                        asset_code: soroban_rs::xdr::AssetCode4(*b"EURC"),
2455                        issuer: create_account_id(TEST_PK),
2456                    }),
2457                    amount: 1_000_000,
2458                }),
2459            };
2460
2461            let tx = Transaction {
2462                source_account: create_muxed_account(TEST_PK),
2463                fee: 100,
2464                seq_num: SequenceNumber(1),
2465                cond: soroban_rs::xdr::Preconditions::None,
2466                memo: soroban_rs::xdr::Memo::None,
2467                operations: vec![payment_op].try_into().unwrap(),
2468                ext: TransactionExt::V0,
2469            };
2470
2471            let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
2472                tx,
2473                signatures: vec![].try_into().unwrap(),
2474            });
2475
2476            let policy = create_usdc_policy(); // Only USDC is allowed
2477
2478            let provider = create_mock_provider_with_balance(10_000_000_000);
2479            let dex_service = create_mock_dex_service();
2480
2481            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2482                &envelope,
2483                TEST_PK_2,
2484                &policy,
2485                &provider,
2486                &dex_service,
2487            )
2488            .await;
2489
2490            assert!(result.is_err());
2491            assert!(result
2492                .unwrap_err()
2493                .to_string()
2494                .contains("not in allowed tokens list"));
2495        }
2496
2497        #[tokio::test]
2498        async fn test_fee_exceeds_token_max() {
2499            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2500            let usdc_asset = format!("USDC:{}", USDC_ISSUER);
2501            let mut policy = RelayerStellarPolicy::default();
2502            policy.allowed_tokens = Some(vec![StellarAllowedTokensPolicy {
2503                asset: usdc_asset,
2504                metadata: None,
2505                swap_config: None,
2506                max_allowed_fee: Some(500_000), // Lower than payment amount
2507            }]);
2508
2509            let provider = create_mock_provider_with_balance(10_000_000_000);
2510            let dex_service = create_mock_dex_service();
2511
2512            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2513                &envelope,
2514                TEST_PK_2,
2515                &policy,
2516                &provider,
2517                &dex_service,
2518            )
2519            .await;
2520
2521            assert!(result.is_err());
2522            assert!(result.unwrap_err().to_string().contains("Max fee exceeded"));
2523        }
2524
2525        #[tokio::test]
2526        async fn test_insufficient_payment_amount() {
2527            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2528            let policy = create_usdc_policy();
2529            let provider = create_mock_provider_with_balance(10_000_000_000);
2530
2531            // Mock DEX to require more than the payment amount
2532            let mut dex_service = MockStellarDexServiceTrait::new();
2533            dex_service
2534                .expect_get_xlm_to_token_quote()
2535                .returning(|_, _, _, _| {
2536                    Box::pin(ready(Ok(
2537                        crate::services::stellar_dex::StellarQuoteResponse {
2538                            input_asset: "native".to_string(),
2539                            output_asset: "USDC:...".to_string(),
2540                            in_amount: 200,
2541                            out_amount: 2_000_000, // More than the 1M payment
2542                            price_impact_pct: 0.0,
2543                            slippage_bps: 100,
2544                            path: None,
2545                        },
2546                    )))
2547                });
2548
2549            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2550                &envelope,
2551                TEST_PK_2,
2552                &policy,
2553                &provider,
2554                &dex_service,
2555            )
2556            .await;
2557
2558            assert!(result.is_err());
2559            assert!(result
2560                .unwrap_err()
2561                .to_string()
2562                .contains("Insufficient token payment"));
2563        }
2564
2565        #[tokio::test]
2566        async fn test_insufficient_user_balance() {
2567            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2568            let policy = create_usdc_policy();
2569
2570            // Create provider with low USDC trustline balance
2571            let mut provider = MockStellarProviderTrait::new();
2572
2573            provider.expect_get_account().returning(move |_| {
2574                Box::pin(ready(Ok(AccountEntry {
2575                    account_id: create_account_id(TEST_PK),
2576                    balance: 10_000_000_000,
2577                    seq_num: SequenceNumber(1),
2578                    num_sub_entries: 0,
2579                    inflation_dest: None,
2580                    flags: 0,
2581                    home_domain: Default::default(),
2582                    thresholds: Thresholds([0; 4]),
2583                    signers: Default::default(),
2584                    ext: AccountEntryExt::V0,
2585                })))
2586            });
2587
2588            provider.expect_get_latest_ledger().returning(|| {
2589                Box::pin(ready(Ok(GetLatestLedgerResponse {
2590                    id: "test".to_string(),
2591                    protocol_version: 20,
2592                    sequence: 1000,
2593                })))
2594            });
2595
2596            provider
2597                .expect_simulate_transaction_envelope()
2598                .returning(|_| {
2599                    Box::pin(ready(Ok(SimulateTransactionResponse {
2600                        min_resource_fee: 100,
2601                        transaction_data: String::new(),
2602                        ..Default::default()
2603                    })))
2604                });
2605
2606            // Mock get_ledger_entries with low USDC balance
2607            provider.expect_get_ledger_entries().returning(|_| {
2608                use soroban_rs::stellar_rpc_client::{GetLedgerEntriesResponse, LedgerEntryResult};
2609                use soroban_rs::xdr::{
2610                    LedgerEntry, LedgerEntryData, LedgerEntryExt, TrustLineAsset, TrustLineEntry,
2611                    TrustLineEntryExt,
2612                };
2613
2614                let trustline_entry = TrustLineEntry {
2615                    account_id: create_account_id(TEST_PK),
2616                    asset: TrustLineAsset::CreditAlphanum4(soroban_rs::xdr::AlphaNum4 {
2617                        asset_code: soroban_rs::xdr::AssetCode4(*b"USDC"),
2618                        issuer: create_account_id(USDC_ISSUER),
2619                    }),
2620                    balance: 500_000, // Only 0.05 USDC - insufficient
2621                    limit: 1_000_000_000,
2622                    flags: 1,
2623                    ext: TrustLineEntryExt::V0,
2624                };
2625
2626                let ledger_entry = LedgerEntry {
2627                    last_modified_ledger_seq: 0,
2628                    data: LedgerEntryData::Trustline(trustline_entry),
2629                    ext: LedgerEntryExt::V0,
2630                };
2631
2632                let xdr_base64 = ledger_entry
2633                    .data
2634                    .to_xdr_base64(soroban_rs::xdr::Limits::none())
2635                    .unwrap();
2636
2637                Box::pin(ready(Ok(GetLedgerEntriesResponse {
2638                    entries: Some(vec![LedgerEntryResult {
2639                        key: String::new(),
2640                        xdr: xdr_base64,
2641                        last_modified_ledger: 0,
2642                        live_until_ledger_seq_ledger_seq: None,
2643                    }]),
2644                    latest_ledger: 0,
2645                })))
2646            });
2647
2648            let dex_service = create_mock_dex_service();
2649
2650            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2651                &envelope,
2652                TEST_PK_2,
2653                &policy,
2654                &provider,
2655                &dex_service,
2656            )
2657            .await;
2658
2659            assert!(result.is_err());
2660            assert!(result
2661                .unwrap_err()
2662                .to_string()
2663                .contains("Insufficient balance"));
2664        }
2665
2666        #[tokio::test]
2667        async fn test_valid_fee_payment_with_usdc() {
2668            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2669            let policy = create_usdc_policy();
2670            let provider = create_mock_provider_with_balance(10_000_000_000);
2671            let dex_service = create_mock_dex_service();
2672
2673            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2674                &envelope,
2675                TEST_PK_2,
2676                &policy,
2677                &provider,
2678                &dex_service,
2679            )
2680            .await;
2681
2682            assert!(result.is_ok());
2683        }
2684
2685        #[tokio::test]
2686        async fn test_dex_conversion_failure() {
2687            let envelope = create_usdc_payment_envelope(TEST_PK, TEST_PK_2, 1_000_000);
2688            let policy = create_usdc_policy();
2689            let provider = create_mock_provider_with_balance(10_000_000_000);
2690
2691            let mut dex_service = MockStellarDexServiceTrait::new();
2692            dex_service
2693                .expect_get_xlm_to_token_quote()
2694                .returning(|_, _, _, _| {
2695                    Box::pin(ready(Err(
2696                        crate::services::stellar_dex::StellarDexServiceError::UnknownError(
2697                            "DEX unavailable".to_string(),
2698                        ),
2699                    )))
2700                });
2701
2702            let result = StellarTransactionValidator::validate_user_fee_payment_amounts(
2703                &envelope,
2704                TEST_PK_2,
2705                &policy,
2706                &provider,
2707                &dex_service,
2708            )
2709            .await;
2710
2711            assert!(result.is_err());
2712            assert!(result
2713                .unwrap_err()
2714                .to_string()
2715                .contains("Failed to convert XLM fee to token"));
2716        }
2717    }
2718
2719    mod validate_contract_invocation_tests {
2720        use super::*;
2721
2722        #[test]
2723        fn test_invoke_contract_allowed() {
2724            let invoke_op = InvokeHostFunctionOp {
2725                host_function: HostFunction::InvokeContract(InvokeContractArgs {
2726                    contract_address: ScAddress::Contract(soroban_rs::xdr::ContractId(
2727                        soroban_rs::xdr::Hash([0u8; 32]),
2728                    )),
2729                    function_name: ScSymbol("test".try_into().unwrap()),
2730                    args: Default::default(),
2731                }),
2732                auth: Default::default(),
2733            };
2734
2735            let policy = RelayerStellarPolicy::default();
2736            assert!(StellarTransactionValidator::validate_contract_invocation(
2737                &invoke_op, 0, TEST_PK_2, &policy
2738            )
2739            .is_ok());
2740        }
2741
2742        #[test]
2743        fn test_create_contract_rejected() {
2744            let invoke_op = InvokeHostFunctionOp {
2745                host_function: HostFunction::CreateContract(soroban_rs::xdr::CreateContractArgs {
2746                    contract_id_preimage: soroban_rs::xdr::ContractIdPreimage::Address(
2747                        soroban_rs::xdr::ContractIdPreimageFromAddress {
2748                            address: ScAddress::Account(create_account_id(TEST_PK)),
2749                            salt: soroban_rs::xdr::Uint256([0u8; 32]),
2750                        },
2751                    ),
2752                    executable: soroban_rs::xdr::ContractExecutable::Wasm(soroban_rs::xdr::Hash(
2753                        [0u8; 32],
2754                    )),
2755                }),
2756                auth: Default::default(),
2757            };
2758
2759            let policy = RelayerStellarPolicy::default();
2760            let result = StellarTransactionValidator::validate_contract_invocation(
2761                &invoke_op, 0, TEST_PK_2, &policy,
2762            );
2763            assert!(result.is_err());
2764            assert!(result
2765                .unwrap_err()
2766                .to_string()
2767                .contains("CreateContract not allowed"));
2768        }
2769
2770        #[test]
2771        fn test_upload_wasm_rejected() {
2772            let invoke_op = InvokeHostFunctionOp {
2773                host_function: HostFunction::UploadContractWasm(vec![0u8; 100].try_into().unwrap()),
2774                auth: Default::default(),
2775            };
2776
2777            let policy = RelayerStellarPolicy::default();
2778            let result = StellarTransactionValidator::validate_contract_invocation(
2779                &invoke_op, 0, TEST_PK_2, &policy,
2780            );
2781            assert!(result.is_err());
2782            assert!(result
2783                .unwrap_err()
2784                .to_string()
2785                .contains("UploadContractWasm not allowed"));
2786        }
2787
2788        #[test]
2789        fn test_relayer_in_auth_rejected() {
2790            let auth_entry = SorobanAuthorizationEntry {
2791                credentials: SorobanCredentials::Address(
2792                    soroban_rs::xdr::SorobanAddressCredentials {
2793                        address: ScAddress::Account(create_account_id(TEST_PK_2)),
2794                        nonce: 0,
2795                        signature_expiration_ledger: 0,
2796                        signature: soroban_rs::xdr::ScVal::Void,
2797                    },
2798                ),
2799                root_invocation: soroban_rs::xdr::SorobanAuthorizedInvocation {
2800                    function: SorobanAuthorizedFunction::ContractFn(
2801                        soroban_rs::xdr::InvokeContractArgs {
2802                            contract_address: ScAddress::Contract(soroban_rs::xdr::ContractId(
2803                                soroban_rs::xdr::Hash([0u8; 32]),
2804                            )),
2805                            function_name: ScSymbol("test".try_into().unwrap()),
2806                            args: Default::default(),
2807                        },
2808                    ),
2809                    sub_invocations: Default::default(),
2810                },
2811            };
2812
2813            let invoke_op = InvokeHostFunctionOp {
2814                host_function: HostFunction::InvokeContract(InvokeContractArgs {
2815                    contract_address: ScAddress::Contract(soroban_rs::xdr::ContractId(
2816                        soroban_rs::xdr::Hash([0u8; 32]),
2817                    )),
2818                    function_name: ScSymbol("test".try_into().unwrap()),
2819                    args: Default::default(),
2820                }),
2821                auth: vec![auth_entry].try_into().unwrap(),
2822            };
2823
2824            let policy = RelayerStellarPolicy::default();
2825            let result = StellarTransactionValidator::validate_contract_invocation(
2826                &invoke_op, 0, TEST_PK_2, // Relayer address matches auth entry
2827                &policy,
2828            );
2829            assert!(result.is_err());
2830            assert!(result.unwrap_err().to_string().contains("requires relayer"));
2831        }
2832    }
2833}