openzeppelin_relayer/domain/relayer/solana/rpc/methods/
validations.rs

1use std::collections::HashMap;
2
3/// Validator for Solana transactions that enforces relayer policies and transaction
4/// constraints.
5///
6/// This validator ensures that transactions meet the following criteria:
7/// * Use allowed programs and accounts
8/// * Have valid blockhash
9/// * Meet size and signature requirements
10/// * Have correct fee payer configuration
11/// * Comply with relayer policies
12use crate::{
13    constants::{DEFAULT_SOLANA_MAX_TX_DATA_SIZE, DEFAULT_SOLANA_MIN_BALANCE},
14    domain::{SolanaTokenProgram, TokenInstruction as SolanaTokenInstruction},
15    models::RelayerSolanaPolicy,
16    services::SolanaProviderTrait,
17};
18use log::info;
19use solana_client::rpc_response::RpcSimulateTransactionResult;
20use solana_sdk::{
21    commitment_config::CommitmentConfig, pubkey::Pubkey, system_instruction::SystemInstruction,
22    transaction::Transaction,
23};
24use solana_system_interface::program;
25use thiserror::Error;
26
27#[derive(Debug, Error)]
28#[allow(dead_code)]
29pub enum SolanaTransactionValidationError {
30    #[error("Failed to decode transaction: {0}")]
31    DecodeError(String),
32    #[error("Failed to deserialize transaction: {0}")]
33    DeserializeError(String),
34    #[error("Validation error: {0}")]
35    SigningError(String),
36    #[error("Simulation error: {0}")]
37    SimulationError(String),
38    #[error("Policy violation: {0}")]
39    PolicyViolation(String),
40    #[error("Blockhash {0} is expired")]
41    ExpiredBlockhash(String),
42    #[error("Validation error: {0}")]
43    ValidationError(String),
44    #[error("Fee payer error: {0}")]
45    FeePayer(String),
46    #[error("Insufficient funds: {0}")]
47    InsufficientFunds(String),
48    #[error("Insufficient balance: {0}")]
49    InsufficientBalance(String),
50}
51
52#[allow(dead_code)]
53pub struct SolanaTransactionValidator {}
54
55#[allow(dead_code)]
56impl SolanaTransactionValidator {
57    pub fn validate_allowed_token(
58        token_mint: &str,
59        policy: &RelayerSolanaPolicy,
60    ) -> Result<(), SolanaTransactionValidationError> {
61        let allowed_token = policy.get_allowed_token_entry(token_mint);
62        if allowed_token.is_none() {
63            return Err(SolanaTransactionValidationError::PolicyViolation(format!(
64                "Token {} not allowed for transfers",
65                token_mint
66            )));
67        }
68
69        Ok(())
70    }
71
72    /// Validates that the transaction's fee payer matches the relayer's address.
73    pub fn validate_fee_payer(
74        tx: &Transaction,
75        relayer_pubkey: &Pubkey,
76    ) -> Result<(), SolanaTransactionValidationError> {
77        // Get fee payer (first account in account_keys)
78        let fee_payer = tx.message.account_keys.first().ok_or_else(|| {
79            SolanaTransactionValidationError::FeePayer("No fee payer account found".to_string())
80        })?;
81
82        // Verify fee payer matches relayer address
83        if fee_payer != relayer_pubkey {
84            return Err(SolanaTransactionValidationError::PolicyViolation(format!(
85                "Fee payer {} does not match relayer address {}",
86                fee_payer, relayer_pubkey
87            )));
88        }
89
90        // Verify fee payer is a signer
91        if tx.message.header.num_required_signatures < 1 {
92            return Err(SolanaTransactionValidationError::FeePayer(
93                "Fee payer must be a signer".to_string(),
94            ));
95        }
96
97        Ok(())
98    }
99
100    /// Validates that the transaction's blockhash is still valid.
101    pub async fn validate_blockhash<T: SolanaProviderTrait>(
102        tx: &Transaction,
103        provider: &T,
104    ) -> Result<(), SolanaTransactionValidationError> {
105        let blockhash = tx.message.recent_blockhash;
106
107        // Check if blockhash is still valid
108        let is_valid = provider
109            .is_blockhash_valid(&blockhash, CommitmentConfig::confirmed())
110            .await
111            .map_err(|e| {
112                SolanaTransactionValidationError::ValidationError(format!(
113                    "Failed to check blockhash validity: {}",
114                    e
115                ))
116            })?;
117
118        if !is_valid {
119            return Err(SolanaTransactionValidationError::ExpiredBlockhash(format!(
120                "Blockhash {} is no longer valid",
121                blockhash
122            )));
123        }
124
125        Ok(())
126    }
127
128    /// Validates the number of required signatures against policy limits.
129    pub fn validate_max_signatures(
130        tx: &Transaction,
131        policy: &RelayerSolanaPolicy,
132    ) -> Result<(), SolanaTransactionValidationError> {
133        let num_signatures = tx.message.header.num_required_signatures;
134
135        let Some(max_signatures) = policy.max_signatures else {
136            return Ok(());
137        };
138
139        if num_signatures > max_signatures {
140            return Err(SolanaTransactionValidationError::PolicyViolation(format!(
141                "Transaction requires {} signatures, which exceeds maximum allowed {}",
142                num_signatures, max_signatures
143            )));
144        }
145
146        Ok(())
147    }
148
149    /// Validates that the transaction's programs are allowed by the relayer's policy.
150    pub fn validate_allowed_programs(
151        tx: &Transaction,
152        policy: &RelayerSolanaPolicy,
153    ) -> Result<(), SolanaTransactionValidationError> {
154        if let Some(allowed_programs) = &policy.allowed_programs {
155            for program_id in tx
156                .message
157                .instructions
158                .iter()
159                .map(|ix| tx.message.account_keys[ix.program_id_index as usize])
160            {
161                if !allowed_programs.contains(&program_id.to_string()) {
162                    return Err(SolanaTransactionValidationError::PolicyViolation(format!(
163                        "Program {} not allowed",
164                        program_id
165                    )));
166                }
167            }
168        }
169
170        Ok(())
171    }
172
173    pub fn validate_allowed_account(
174        account: &str,
175        policy: &RelayerSolanaPolicy,
176    ) -> Result<(), SolanaTransactionValidationError> {
177        if let Some(allowed_accounts) = &policy.allowed_accounts {
178            if !allowed_accounts.contains(&account.to_string()) {
179                return Err(SolanaTransactionValidationError::PolicyViolation(format!(
180                    "Account {} not allowed",
181                    account
182                )));
183            }
184        }
185
186        Ok(())
187    }
188
189    /// Validates that the transaction's accounts are allowed by the relayer's policy.
190    pub fn validate_tx_allowed_accounts(
191        tx: &Transaction,
192        policy: &RelayerSolanaPolicy,
193    ) -> Result<(), SolanaTransactionValidationError> {
194        if let Some(allowed_accounts) = &policy.allowed_accounts {
195            for account_key in &tx.message.account_keys {
196                info!("Checking account {}", account_key);
197                if !allowed_accounts.contains(&account_key.to_string()) {
198                    return Err(SolanaTransactionValidationError::PolicyViolation(format!(
199                        "Account {} not allowed",
200                        account_key
201                    )));
202                }
203            }
204        }
205
206        Ok(())
207    }
208
209    pub fn validate_disallowed_account(
210        account: &str,
211        policy: &RelayerSolanaPolicy,
212    ) -> Result<(), SolanaTransactionValidationError> {
213        if let Some(disallowed_accounts) = &policy.disallowed_accounts {
214            if disallowed_accounts.contains(&account.to_string()) {
215                return Err(SolanaTransactionValidationError::PolicyViolation(format!(
216                    "Account {} not allowed",
217                    account
218                )));
219            }
220        }
221
222        Ok(())
223    }
224
225    /// Validates that the transaction's accounts are not disallowed by the relayer's policy.
226    pub fn validate_tx_disallowed_accounts(
227        tx: &Transaction,
228        policy: &RelayerSolanaPolicy,
229    ) -> Result<(), SolanaTransactionValidationError> {
230        let Some(disallowed_accounts) = &policy.disallowed_accounts else {
231            return Ok(());
232        };
233
234        for account_key in &tx.message.account_keys {
235            if disallowed_accounts.contains(&account_key.to_string()) {
236                return Err(SolanaTransactionValidationError::PolicyViolation(format!(
237                    "Account {} is explicitly disallowed",
238                    account_key
239                )));
240            }
241        }
242
243        Ok(())
244    }
245
246    /// Validates that the transaction's data size is within policy limits.
247    pub fn validate_data_size(
248        tx: &Transaction,
249        config: &RelayerSolanaPolicy,
250    ) -> Result<(), SolanaTransactionValidationError> {
251        let max_size: usize = config
252            .max_tx_data_size
253            .unwrap_or(DEFAULT_SOLANA_MAX_TX_DATA_SIZE)
254            .into();
255        let tx_bytes = bincode::serialize(tx)
256            .map_err(|e| SolanaTransactionValidationError::DeserializeError(e.to_string()))?;
257
258        if tx_bytes.len() > max_size {
259            return Err(SolanaTransactionValidationError::PolicyViolation(format!(
260                "Transaction size {} exceeds maximum allowed {}",
261                tx_bytes.len(),
262                max_size
263            )));
264        }
265        Ok(())
266    }
267
268    /// Validates that the relayer is not used as source in lamports transfers.
269    pub async fn validate_lamports_transfers(
270        tx: &Transaction,
271        relayer_account: &Pubkey,
272    ) -> Result<(), SolanaTransactionValidationError> {
273        // Iterate over each instruction in the transaction
274        for (ix_index, ix) in tx.message.instructions.iter().enumerate() {
275            let program_id = tx.message.account_keys[ix.program_id_index as usize];
276
277            // Check if the instruction comes from the System Program (native SOL transfers)
278            #[allow(clippy::collapsible_match)]
279            if program_id == program::id() {
280                if let Ok(system_ix) = bincode::deserialize::<SystemInstruction>(&ix.data) {
281                    if let SystemInstruction::Transfer { .. } = system_ix {
282                        // In a system transfer instruction, the first account is the source and the
283                        // second is the destination.
284                        let source_index = ix.accounts.first().ok_or_else(|| {
285                            SolanaTransactionValidationError::ValidationError(format!(
286                                "Missing source account in instruction {}",
287                                ix_index
288                            ))
289                        })?;
290                        let source_pubkey = &tx.message.account_keys[*source_index as usize];
291
292                        // Only validate transfers where the source is the relayer fee account.
293                        if source_pubkey == relayer_account {
294                            return Err(SolanaTransactionValidationError::PolicyViolation(
295                                "Lamports transfers are not allowed from the relayer account"
296                                    .to_string(),
297                            ));
298                        }
299                    }
300                }
301            }
302        }
303        Ok(())
304    }
305
306    /// Validates transfer amount against policy limits.
307    pub fn validate_max_fee(
308        amount: u64,
309        policy: &RelayerSolanaPolicy,
310    ) -> Result<(), SolanaTransactionValidationError> {
311        if let Some(max_amount) = policy.max_allowed_fee_lamports {
312            if amount > max_amount {
313                return Err(SolanaTransactionValidationError::PolicyViolation(format!(
314                    "Fee amount {} exceeds max allowed fee amount {}",
315                    amount, max_amount
316                )));
317            }
318        }
319
320        Ok(())
321    }
322
323    /// Validates transfer amount against policy limits.
324    pub async fn validate_sufficient_relayer_balance(
325        fee: u64,
326        relayer_address: &str,
327        policy: &RelayerSolanaPolicy,
328        provider: &impl SolanaProviderTrait,
329    ) -> Result<(), SolanaTransactionValidationError> {
330        let balance = provider
331            .get_balance(relayer_address)
332            .await
333            .map_err(|e| SolanaTransactionValidationError::ValidationError(e.to_string()))?;
334
335        // Ensure minimum balance policy is maintained
336        let min_balance = policy.min_balance.unwrap_or(DEFAULT_SOLANA_MIN_BALANCE);
337        let required_balance = fee + min_balance;
338
339        if balance < required_balance {
340            return Err(SolanaTransactionValidationError::InsufficientBalance(format!(
341                "Insufficient relayer balance. Required: {}, Available: {}, Fee: {}, Min balance: {}",
342                required_balance, balance, fee, min_balance
343            )));
344        }
345
346        Ok(())
347    }
348
349    /// Validates token transfers against policy restrictions.
350    pub async fn validate_token_transfers(
351        tx: &Transaction,
352        policy: &RelayerSolanaPolicy,
353        provider: &impl SolanaProviderTrait,
354        relayer_account: &Pubkey,
355    ) -> Result<(), SolanaTransactionValidationError> {
356        let allowed_tokens = match &policy.allowed_tokens {
357            Some(tokens) if !tokens.is_empty() => tokens,
358            _ => return Ok(()), // No token restrictions
359        };
360
361        // Track cumulative transfers from each source account
362        let mut account_transfers: HashMap<Pubkey, u64> = HashMap::new();
363        let mut account_balances: HashMap<Pubkey, u64> = HashMap::new();
364
365        for ix in &tx.message.instructions {
366            let program_id = tx.message.account_keys[ix.program_id_index as usize];
367
368            if !SolanaTokenProgram::is_token_program(&program_id) {
369                continue;
370            }
371
372            let token_ix = match SolanaTokenProgram::unpack_instruction(&program_id, &ix.data) {
373                Ok(ix) => ix,
374                Err(_) => continue, // Skip instructions we can't decode
375            };
376
377            // Decode token instruction
378            match token_ix {
379                SolanaTokenInstruction::Transfer { amount }
380                | SolanaTokenInstruction::TransferChecked { amount, .. } => {
381                    // Get source account info
382                    let source_index = ix.accounts[0] as usize;
383                    let source_pubkey = &tx.message.account_keys[source_index];
384
385                    // Validate source account is writable but not signer
386                    if !tx.message.is_maybe_writable(source_index, None) {
387                        return Err(SolanaTransactionValidationError::ValidationError(
388                            "Source account must be writable".to_string(),
389                        ));
390                    }
391                    if tx.message.is_signer(source_index) {
392                        return Err(SolanaTransactionValidationError::ValidationError(
393                            "Source account must not be signer".to_string(),
394                        ));
395                    }
396
397                    if source_pubkey == relayer_account {
398                        return Err(SolanaTransactionValidationError::PolicyViolation(
399                            "Relayer account cannot be source".to_string(),
400                        ));
401                    }
402
403                    let dest_index = match token_ix {
404                        SolanaTokenInstruction::TransferChecked { .. } => ix.accounts[2] as usize,
405                        _ => ix.accounts[1] as usize,
406                    };
407                    let destination_pubkey = &tx.message.account_keys[dest_index];
408
409                    // Validate destination account is writable but not signer
410                    if !tx.message.is_maybe_writable(dest_index, None) {
411                        return Err(SolanaTransactionValidationError::ValidationError(
412                            "Destination account must be writable".to_string(),
413                        ));
414                    }
415                    if tx.message.is_signer(dest_index) {
416                        return Err(SolanaTransactionValidationError::ValidationError(
417                            "Destination account must not be signer".to_string(),
418                        ));
419                    }
420
421                    let owner_index = match token_ix {
422                        SolanaTokenInstruction::TransferChecked { .. } => ix.accounts[3] as usize,
423                        _ => ix.accounts[2] as usize,
424                    };
425                    // Validate owner is signer but not writable
426                    if !tx.message.is_signer(owner_index) {
427                        return Err(SolanaTransactionValidationError::ValidationError(format!(
428                            "Owner must be signer {}",
429                            &tx.message.account_keys[owner_index]
430                        )));
431                    }
432
433                    // Get mint address from token account - only once per source account
434                    if !account_balances.contains_key(source_pubkey) {
435                        let source_account = provider
436                            .get_account_from_pubkey(source_pubkey)
437                            .await
438                            .map_err(|e| {
439                                SolanaTransactionValidationError::ValidationError(e.to_string())
440                            })?;
441
442                        let token_account =
443                            SolanaTokenProgram::unpack_account(&program_id, &source_account)
444                                .map_err(|e| {
445                                    SolanaTransactionValidationError::ValidationError(format!(
446                                        "Invalid token account: {}",
447                                        e
448                                    ))
449                                })?;
450
451                        if token_account.is_frozen {
452                            return Err(SolanaTransactionValidationError::PolicyViolation(
453                                "Token account is frozen".to_string(),
454                            ));
455                        }
456
457                        let token_config = allowed_tokens
458                            .iter()
459                            .find(|t| t.mint == token_account.mint.to_string());
460
461                        // check if token is allowed by policy
462                        if token_config.is_none() {
463                            return Err(SolanaTransactionValidationError::PolicyViolation(
464                                format!("Token {} not allowed for transfers", token_account.mint),
465                            ));
466                        }
467                        // Store the balance for later use
468                        account_balances.insert(*source_pubkey, token_account.amount);
469
470                        // Validate decimals for TransferChecked
471                        if let (
472                            Some(config),
473                            SolanaTokenInstruction::TransferChecked { decimals, .. },
474                        ) = (token_config, &token_ix)
475                        {
476                            if Some(*decimals) != config.decimals {
477                                return Err(SolanaTransactionValidationError::ValidationError(
478                                    format!(
479                                        "Invalid decimals: expected {:?}, got {}",
480                                        config.decimals, decimals
481                                    ),
482                                ));
483                            }
484                        }
485
486                        // if relayer is destination, check max fee
487                        if destination_pubkey == relayer_account {
488                            // Check max fee if configured
489                            if let Some(config) = token_config {
490                                if let Some(max_fee) = config.max_allowed_fee {
491                                    if amount > max_fee {
492                                        return Err(
493                                            SolanaTransactionValidationError::PolicyViolation(
494                                                format!(
495                                                    "Transfer amount {} exceeds max fee \
496                                                    allowed {} for token {}",
497                                                    amount, max_fee, token_account.mint
498                                                ),
499                                            ),
500                                        );
501                                    }
502                                }
503                            }
504                        }
505                    }
506
507                    *account_transfers.entry(*source_pubkey).or_insert(0) += amount;
508                }
509                _ => {
510                    // For any other token instruction, verify relayer account is not used
511                    // as a source by checking if it's marked as writable
512                    for account in ix.accounts.iter() {
513                        let account_index = *account as usize;
514                        if account_index < tx.message.account_keys.len() {
515                            let pubkey = &tx.message.account_keys[account_index];
516                            if pubkey == relayer_account
517                                && tx.message.is_maybe_writable(account_index, None)
518                                && !tx.message.is_signer(account_index)
519                            {
520                                // It's ok if relayer is just signing
521                                return Err(SolanaTransactionValidationError::PolicyViolation(
522                                            "Relayer account cannot be used as writable account in token instructions".to_string(),
523                                        ));
524                            }
525                        }
526                    }
527                }
528            }
529        }
530
531        // validate that cumulative transfers don't exceed balances
532        for (account, total_transfer) in account_transfers {
533            let balance = *account_balances.get(&account).unwrap();
534
535            if balance < total_transfer {
536                return Err(SolanaTransactionValidationError::ValidationError(
537                    format!(
538                        "Insufficient balance for cumulative transfers: account {} has balance {} but requires {} across all instructions",
539                        account, balance, total_transfer
540                    ),
541                ));
542            }
543        }
544        Ok(())
545    }
546
547    /// Simulates transaction
548    pub async fn simulate_transaction<T: SolanaProviderTrait>(
549        tx: &Transaction,
550        provider: &T,
551    ) -> Result<RpcSimulateTransactionResult, SolanaTransactionValidationError> {
552        let new_tx = Transaction::new_unsigned(tx.message.clone());
553
554        provider
555            .simulate_transaction(&new_tx)
556            .await
557            .map_err(|e| SolanaTransactionValidationError::SimulationError(e.to_string()))
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    use crate::{
564        models::{relayer::SolanaAllowedTokensSwapConfig, SolanaAllowedTokensPolicy},
565        services::{MockSolanaProviderTrait, SolanaProviderError},
566    };
567
568    use super::*;
569    use mockall::predicate::*;
570    use solana_sdk::{
571        instruction::{AccountMeta, Instruction},
572        message::Message,
573        program_pack::Pack,
574        signature::{Keypair, Signer},
575    };
576    use solana_system_interface::{instruction, program};
577    use spl_token::{instruction as token_instruction, state::Account};
578
579    fn setup_token_transfer_test(
580        transfer_amount: Option<u64>,
581    ) -> (
582        Transaction,
583        RelayerSolanaPolicy,
584        MockSolanaProviderTrait,
585        Keypair, // source owner
586        Pubkey,  // token mint
587        Pubkey,  // source token account
588        Pubkey,  // destination token account
589    ) {
590        let owner = Keypair::new();
591        let mint = Pubkey::new_unique();
592        let source = Pubkey::new_unique();
593        let destination = Pubkey::new_unique();
594
595        // Create token transfer instruction
596        let transfer_ix = token_instruction::transfer(
597            &spl_token::id(),
598            &source,
599            &destination,
600            &owner.pubkey(),
601            &[],
602            transfer_amount.unwrap_or(100),
603        )
604        .unwrap();
605
606        let message = Message::new(&[transfer_ix], Some(&owner.pubkey()));
607        let mut transaction = Transaction::new_unsigned(message);
608
609        // Ensure owner is marked as signer but not writable
610        if let Some(owner_index) = transaction
611            .message
612            .account_keys
613            .iter()
614            .position(|&pubkey| pubkey == owner.pubkey())
615        {
616            transaction.message.header.num_required_signatures = (owner_index + 1) as u8;
617            transaction.message.header.num_readonly_signed_accounts = 1;
618        }
619
620        let policy = RelayerSolanaPolicy {
621            allowed_tokens: Some(vec![SolanaAllowedTokensPolicy {
622                mint: mint.to_string(),
623                decimals: Some(9),
624                symbol: Some("USDC".to_string()),
625                max_allowed_fee: Some(100),
626                swap_config: Some(SolanaAllowedTokensSwapConfig {
627                    ..Default::default()
628                }),
629            }]),
630            ..Default::default()
631        };
632
633        let mut mock_provider = MockSolanaProviderTrait::new();
634
635        // Setup default mock responses
636        let token_account = Account {
637            mint,
638            owner: owner.pubkey(),
639            amount: 999,
640            state: spl_token::state::AccountState::Initialized,
641            ..Default::default()
642        };
643        let mut account_data = vec![0; Account::LEN];
644        Account::pack(token_account, &mut account_data).unwrap();
645
646        mock_provider
647            .expect_get_account_from_pubkey()
648            .returning(move |_| {
649                let local_account_data = account_data.clone();
650                Box::pin(async move {
651                    Ok(solana_sdk::account::Account {
652                        lamports: 1000000,
653                        data: local_account_data,
654                        owner: spl_token::id(),
655                        executable: false,
656                        rent_epoch: 0,
657                    })
658                })
659            });
660
661        (
662            transaction,
663            policy,
664            mock_provider,
665            owner,
666            mint,
667            source,
668            destination,
669        )
670    }
671
672    fn create_test_transaction(fee_payer: &Pubkey) -> Transaction {
673        let recipient = Pubkey::new_unique();
674        let instruction = instruction::transfer(fee_payer, &recipient, 1000);
675        let message = Message::new(&[instruction], Some(fee_payer));
676        Transaction::new_unsigned(message)
677    }
678
679    #[test]
680    fn test_validate_fee_payer_success() {
681        let relayer_keypair = Keypair::new();
682        let relayer_address = relayer_keypair.pubkey();
683        let tx = create_test_transaction(&relayer_address);
684
685        let result = SolanaTransactionValidator::validate_fee_payer(&tx, &relayer_address);
686
687        assert!(result.is_ok());
688    }
689
690    #[test]
691    fn test_validate_fee_payer_mismatch() {
692        let wrong_keypair = Keypair::new();
693        let relayer_address = Keypair::new().pubkey();
694
695        let tx = create_test_transaction(&wrong_keypair.pubkey());
696
697        let result = SolanaTransactionValidator::validate_fee_payer(&tx, &relayer_address);
698        assert!(matches!(
699            result.unwrap_err(),
700            SolanaTransactionValidationError::PolicyViolation(_)
701        ));
702    }
703
704    #[tokio::test]
705    async fn test_validate_blockhash_valid() {
706        let transaction = create_test_transaction(&Keypair::new().pubkey());
707        let mut mock_provider = MockSolanaProviderTrait::new();
708
709        mock_provider
710            .expect_is_blockhash_valid()
711            .with(
712                eq(transaction.message.recent_blockhash),
713                eq(CommitmentConfig::confirmed()),
714            )
715            .returning(|_, _| Box::pin(async { Ok(true) }));
716
717        let result =
718            SolanaTransactionValidator::validate_blockhash(&transaction, &mock_provider).await;
719
720        assert!(result.is_ok());
721    }
722
723    #[tokio::test]
724    async fn test_validate_blockhash_expired() {
725        let transaction = create_test_transaction(&Keypair::new().pubkey());
726        let mut mock_provider = MockSolanaProviderTrait::new();
727
728        mock_provider
729            .expect_is_blockhash_valid()
730            .returning(|_, _| Box::pin(async { Ok(false) }));
731
732        let result =
733            SolanaTransactionValidator::validate_blockhash(&transaction, &mock_provider).await;
734
735        assert!(matches!(
736            result.unwrap_err(),
737            SolanaTransactionValidationError::ExpiredBlockhash(_)
738        ));
739    }
740
741    #[tokio::test]
742    async fn test_validate_blockhash_provider_error() {
743        let transaction = create_test_transaction(&Keypair::new().pubkey());
744        let mut mock_provider = MockSolanaProviderTrait::new();
745
746        mock_provider.expect_is_blockhash_valid().returning(|_, _| {
747            Box::pin(async { Err(SolanaProviderError::RpcError("RPC error".to_string())) })
748        });
749
750        let result =
751            SolanaTransactionValidator::validate_blockhash(&transaction, &mock_provider).await;
752
753        assert!(matches!(
754            result.unwrap_err(),
755            SolanaTransactionValidationError::ValidationError(_)
756        ));
757    }
758
759    #[test]
760    fn test_validate_max_signatures_within_limit() {
761        let transaction = create_test_transaction(&Keypair::new().pubkey());
762        let policy = RelayerSolanaPolicy {
763            max_signatures: Some(2),
764            ..Default::default()
765        };
766
767        let result = SolanaTransactionValidator::validate_max_signatures(&transaction, &policy);
768        assert!(result.is_ok());
769    }
770
771    #[test]
772    fn test_validate_max_signatures_exceeds_limit() {
773        let transaction = create_test_transaction(&Keypair::new().pubkey());
774        let policy = RelayerSolanaPolicy {
775            max_signatures: Some(0),
776            ..Default::default()
777        };
778
779        let result = SolanaTransactionValidator::validate_max_signatures(&transaction, &policy);
780        assert!(matches!(
781            result.unwrap_err(),
782            SolanaTransactionValidationError::PolicyViolation(_)
783        ));
784    }
785
786    #[test]
787    fn test_validate_max_signatures_no_limit() {
788        let transaction = create_test_transaction(&Keypair::new().pubkey());
789        let policy = RelayerSolanaPolicy {
790            max_signatures: None,
791            ..Default::default()
792        };
793
794        let result = SolanaTransactionValidator::validate_max_signatures(&transaction, &policy);
795        assert!(result.is_ok());
796    }
797
798    #[test]
799    fn test_validate_max_signatures_exact_limit() {
800        let transaction = create_test_transaction(&Keypair::new().pubkey());
801        let policy = RelayerSolanaPolicy {
802            max_signatures: Some(1),
803            ..Default::default()
804        };
805
806        let result = SolanaTransactionValidator::validate_max_signatures(&transaction, &policy);
807        assert!(result.is_ok());
808    }
809
810    #[test]
811    fn test_validate_allowed_programs_success() {
812        let payer = Keypair::new();
813        let tx = create_test_transaction(&payer.pubkey());
814        let policy = RelayerSolanaPolicy {
815            allowed_programs: Some(vec![program::id().to_string()]),
816            ..Default::default()
817        };
818
819        let result = SolanaTransactionValidator::validate_allowed_programs(&tx, &policy);
820        assert!(result.is_ok());
821    }
822
823    #[test]
824    fn test_validate_allowed_programs_disallowed() {
825        let payer = Keypair::new();
826        let tx = create_test_transaction(&payer.pubkey());
827
828        let policy = RelayerSolanaPolicy {
829            allowed_programs: Some(vec![Pubkey::new_unique().to_string()]),
830            ..Default::default()
831        };
832
833        let result = SolanaTransactionValidator::validate_allowed_programs(&tx, &policy);
834        assert!(matches!(
835            result.unwrap_err(),
836            SolanaTransactionValidationError::PolicyViolation(_)
837        ));
838    }
839
840    #[test]
841    fn test_validate_allowed_programs_no_restrictions() {
842        let payer = Keypair::new();
843        let tx = create_test_transaction(&payer.pubkey());
844
845        let policy = RelayerSolanaPolicy {
846            allowed_programs: None,
847            ..Default::default()
848        };
849
850        let result = SolanaTransactionValidator::validate_allowed_programs(&tx, &policy);
851        assert!(result.is_ok());
852    }
853
854    #[test]
855    fn test_validate_allowed_programs_multiple_instructions() {
856        let payer = Keypair::new();
857        let recipient = Pubkey::new_unique();
858
859        let ix1 = instruction::transfer(&payer.pubkey(), &recipient, 1000);
860        let ix2 = instruction::transfer(&payer.pubkey(), &recipient, 2000);
861        let message = Message::new(&[ix1, ix2], Some(&payer.pubkey()));
862        let tx = Transaction::new_unsigned(message);
863
864        let policy = RelayerSolanaPolicy {
865            allowed_programs: Some(vec![program::id().to_string()]),
866            ..Default::default()
867        };
868
869        let result = SolanaTransactionValidator::validate_allowed_programs(&tx, &policy);
870        assert!(result.is_ok());
871    }
872
873    #[test]
874    fn test_validate_tx_allowed_accounts_success() {
875        let payer = Keypair::new();
876        let recipient = Pubkey::new_unique();
877
878        let ix = instruction::transfer(&payer.pubkey(), &recipient, 1000);
879        let message = Message::new(&[ix], Some(&payer.pubkey()));
880        let tx = Transaction::new_unsigned(message);
881
882        let policy = RelayerSolanaPolicy {
883            allowed_accounts: Some(vec![
884                payer.pubkey().to_string(),
885                recipient.to_string(),
886                program::id().to_string(),
887            ]),
888            ..Default::default()
889        };
890
891        let result = SolanaTransactionValidator::validate_tx_allowed_accounts(&tx, &policy);
892        assert!(result.is_ok());
893    }
894
895    #[test]
896    fn test_validate_tx_allowed_accounts_disallowed() {
897        let payer = Keypair::new();
898
899        let tx = create_test_transaction(&payer.pubkey());
900
901        let policy = RelayerSolanaPolicy {
902            allowed_accounts: Some(vec![payer.pubkey().to_string()]),
903            ..Default::default()
904        };
905
906        let result = SolanaTransactionValidator::validate_tx_allowed_accounts(&tx, &policy);
907        assert!(matches!(
908            result.unwrap_err(),
909            SolanaTransactionValidationError::PolicyViolation(_)
910        ));
911    }
912
913    #[test]
914    fn test_validate_tx_allowed_accounts_no_restrictions() {
915        let tx = create_test_transaction(&Keypair::new().pubkey());
916
917        let policy = RelayerSolanaPolicy {
918            allowed_accounts: None,
919            ..Default::default()
920        };
921
922        let result = SolanaTransactionValidator::validate_tx_allowed_accounts(&tx, &policy);
923        assert!(result.is_ok());
924    }
925
926    #[test]
927    fn test_validate_tx_allowed_accounts_system_program() {
928        let payer = Keypair::new();
929        let tx = create_test_transaction(&payer.pubkey());
930
931        let policy = RelayerSolanaPolicy {
932            allowed_accounts: Some(vec![payer.pubkey().to_string(), program::id().to_string()]),
933            ..Default::default()
934        };
935
936        let result = SolanaTransactionValidator::validate_tx_allowed_accounts(&tx, &policy);
937        assert!(matches!(
938            result.unwrap_err(),
939            SolanaTransactionValidationError::PolicyViolation(_)
940        ));
941    }
942
943    #[test]
944    fn test_validate_tx_disallowed_accounts_success() {
945        let payer = Keypair::new();
946
947        let tx = create_test_transaction(&payer.pubkey());
948
949        let policy = RelayerSolanaPolicy {
950            disallowed_accounts: Some(vec![Pubkey::new_unique().to_string()]),
951            ..Default::default()
952        };
953
954        let result = SolanaTransactionValidator::validate_tx_disallowed_accounts(&tx, &policy);
955        assert!(result.is_ok());
956    }
957
958    #[test]
959    fn test_validate_tx_disallowed_accounts_blocked() {
960        let payer = Keypair::new();
961        let recipient = Pubkey::new_unique();
962
963        let ix = instruction::transfer(&payer.pubkey(), &recipient, 1000);
964        let message = Message::new(&[ix], Some(&payer.pubkey()));
965        let tx = Transaction::new_unsigned(message);
966
967        let policy = RelayerSolanaPolicy {
968            disallowed_accounts: Some(vec![recipient.to_string()]),
969            ..Default::default()
970        };
971
972        let result = SolanaTransactionValidator::validate_tx_disallowed_accounts(&tx, &policy);
973        assert!(matches!(
974            result.unwrap_err(),
975            SolanaTransactionValidationError::PolicyViolation(_)
976        ));
977    }
978
979    #[test]
980    fn test_validate_tx_disallowed_accounts_no_restrictions() {
981        let tx = create_test_transaction(&Keypair::new().pubkey());
982
983        let policy = RelayerSolanaPolicy {
984            disallowed_accounts: None,
985            ..Default::default()
986        };
987
988        let result = SolanaTransactionValidator::validate_tx_disallowed_accounts(&tx, &policy);
989        assert!(result.is_ok());
990    }
991
992    #[test]
993    fn test_validate_tx_disallowed_accounts_system_program() {
994        let payer = Keypair::new();
995        let tx = create_test_transaction(&payer.pubkey());
996
997        let policy = RelayerSolanaPolicy {
998            disallowed_accounts: Some(vec![program::id().to_string()]),
999            ..Default::default()
1000        };
1001
1002        let result = SolanaTransactionValidator::validate_tx_disallowed_accounts(&tx, &policy);
1003        assert!(matches!(
1004            result.unwrap_err(),
1005            SolanaTransactionValidationError::PolicyViolation(_)
1006        ));
1007    }
1008
1009    #[test]
1010    fn test_validate_data_size_within_limit() {
1011        let payer = Keypair::new();
1012        let tx = create_test_transaction(&payer.pubkey());
1013
1014        let policy = RelayerSolanaPolicy {
1015            max_tx_data_size: Some(1500),
1016            ..Default::default()
1017        };
1018
1019        let result = SolanaTransactionValidator::validate_data_size(&tx, &policy);
1020        assert!(result.is_ok());
1021    }
1022
1023    #[test]
1024    fn test_validate_data_size_exceeds_limit() {
1025        let payer = Keypair::new();
1026        let tx = create_test_transaction(&payer.pubkey());
1027
1028        let policy = RelayerSolanaPolicy {
1029            max_tx_data_size: Some(10),
1030            ..Default::default()
1031        };
1032
1033        let result = SolanaTransactionValidator::validate_data_size(&tx, &policy);
1034        assert!(matches!(
1035            result.unwrap_err(),
1036            SolanaTransactionValidationError::PolicyViolation(_)
1037        ));
1038    }
1039
1040    #[test]
1041    fn test_validate_data_size_large_instruction() {
1042        let payer = Keypair::new();
1043        let recipient = Pubkey::new_unique();
1044
1045        let large_data = vec![0u8; 1000];
1046        let ix = Instruction::new_with_bytes(
1047            program::id(),
1048            &large_data,
1049            vec![
1050                AccountMeta::new(payer.pubkey(), true),
1051                AccountMeta::new(recipient, false),
1052            ],
1053        );
1054
1055        let message = Message::new(&[ix], Some(&payer.pubkey()));
1056        let tx = Transaction::new_unsigned(message);
1057
1058        let policy = RelayerSolanaPolicy {
1059            max_tx_data_size: Some(500),
1060            ..Default::default()
1061        };
1062
1063        let result = SolanaTransactionValidator::validate_data_size(&tx, &policy);
1064        assert!(matches!(
1065            result.unwrap_err(),
1066            SolanaTransactionValidationError::PolicyViolation(_)
1067        ));
1068    }
1069
1070    #[test]
1071    fn test_validate_data_size_multiple_instructions() {
1072        let payer = Keypair::new();
1073        let recipient = Pubkey::new_unique();
1074
1075        let ix1 = instruction::transfer(&payer.pubkey(), &recipient, 1000);
1076        let ix2 = instruction::transfer(&payer.pubkey(), &recipient, 2000);
1077        let message = Message::new(&[ix1, ix2], Some(&payer.pubkey()));
1078        let tx = Transaction::new_unsigned(message);
1079
1080        let policy = RelayerSolanaPolicy {
1081            max_tx_data_size: Some(1500),
1082            ..Default::default()
1083        };
1084
1085        let result = SolanaTransactionValidator::validate_data_size(&tx, &policy);
1086        assert!(result.is_ok());
1087    }
1088
1089    #[tokio::test]
1090    async fn test_simulate_transaction_success() {
1091        let transaction = create_test_transaction(&Keypair::new().pubkey());
1092        let mut mock_provider = MockSolanaProviderTrait::new();
1093
1094        mock_provider
1095            .expect_simulate_transaction()
1096            .with(eq(transaction.clone()))
1097            .returning(move |_| {
1098                let simulation_result = RpcSimulateTransactionResult {
1099                    err: None,
1100                    logs: Some(vec!["Program log: success".to_string()]),
1101                    accounts: None,
1102                    units_consumed: Some(100000),
1103                    return_data: None,
1104                    inner_instructions: None,
1105                    replacement_blockhash: None,
1106                    loaded_accounts_data_size: None,
1107                };
1108                Box::pin(async { Ok(simulation_result) })
1109            });
1110
1111        let result =
1112            SolanaTransactionValidator::simulate_transaction(&transaction, &mock_provider).await;
1113
1114        assert!(result.is_ok());
1115        let simulation = result.unwrap();
1116        assert!(simulation.err.is_none());
1117        assert_eq!(simulation.units_consumed, Some(100000));
1118    }
1119
1120    #[tokio::test]
1121    async fn test_simulate_transaction_failure() {
1122        let transaction = create_test_transaction(&Keypair::new().pubkey());
1123        let mut mock_provider = MockSolanaProviderTrait::new();
1124
1125        mock_provider.expect_simulate_transaction().returning(|_| {
1126            Box::pin(async {
1127                Err(SolanaProviderError::RpcError(
1128                    "Simulation failed".to_string(),
1129                ))
1130            })
1131        });
1132
1133        let result =
1134            SolanaTransactionValidator::simulate_transaction(&transaction, &mock_provider).await;
1135
1136        assert!(matches!(
1137            result.unwrap_err(),
1138            SolanaTransactionValidationError::SimulationError(_)
1139        ));
1140    }
1141
1142    #[tokio::test]
1143    async fn test_validate_token_transfers_success() {
1144        let (tx, policy, provider, ..) = setup_token_transfer_test(Some(100));
1145
1146        let result = SolanaTransactionValidator::validate_token_transfers(
1147            &tx,
1148            &policy,
1149            &provider,
1150            &Pubkey::new_unique(),
1151        )
1152        .await;
1153
1154        assert!(result.is_ok());
1155    }
1156
1157    #[tokio::test]
1158    async fn test_validate_token_transfers_insufficient_balance() {
1159        let (tx, policy, provider, ..) = setup_token_transfer_test(Some(2000));
1160
1161        let result = SolanaTransactionValidator::validate_token_transfers(
1162            &tx,
1163            &policy,
1164            &provider,
1165            &Pubkey::new_unique(),
1166        )
1167        .await;
1168
1169        match result {
1170            Err(SolanaTransactionValidationError::ValidationError(msg)) => {
1171                assert!(
1172                    msg.contains("Insufficient balance for cumulative transfers: account "),
1173                    "Unexpected error message: {}",
1174                    msg
1175                );
1176                assert!(
1177                    msg.contains("has balance 999 but requires 2000 across all instructions"),
1178                    "Unexpected error message: {}",
1179                    msg
1180                );
1181            }
1182            other => panic!(
1183                "Expected ValidationError for insufficient balance, got {:?}",
1184                other
1185            ),
1186        }
1187    }
1188
1189    #[tokio::test]
1190    async fn test_validate_token_transfers_relayer_max_fee() {
1191        let (tx, policy, provider, _owner, _mint, _source, destination) =
1192            setup_token_transfer_test(Some(500));
1193
1194        let result = SolanaTransactionValidator::validate_token_transfers(
1195            &tx,
1196            &policy,
1197            &provider,
1198            &destination,
1199        )
1200        .await;
1201
1202        match result {
1203            Err(SolanaTransactionValidationError::PolicyViolation(msg)) => {
1204                assert!(
1205                    msg.contains("Transfer amount 500 exceeds max fee allowed 100"),
1206                    "Unexpected error message: {}",
1207                    msg
1208                );
1209            }
1210            other => panic!(
1211                "Expected ValidationError for insufficient balance, got {:?}",
1212                other
1213            ),
1214        }
1215    }
1216
1217    #[tokio::test]
1218    async fn test_validate_token_transfers_relayer_max_fee_not_applied_for_secondary_accounts() {
1219        let (tx, policy, provider, ..) = setup_token_transfer_test(Some(500));
1220
1221        let result = SolanaTransactionValidator::validate_token_transfers(
1222            &tx,
1223            &policy,
1224            &provider,
1225            &Pubkey::new_unique(),
1226        )
1227        .await;
1228
1229        assert!(result.is_ok());
1230    }
1231
1232    #[tokio::test]
1233    async fn test_validate_token_transfers_disallowed_token() {
1234        let (tx, mut policy, provider, ..) = setup_token_transfer_test(Some(100));
1235
1236        policy.allowed_tokens = Some(vec![SolanaAllowedTokensPolicy {
1237            mint: Pubkey::new_unique().to_string(), // Different mint
1238            decimals: Some(9),
1239            symbol: Some("USDT".to_string()),
1240            max_allowed_fee: None,
1241            swap_config: Some(SolanaAllowedTokensSwapConfig {
1242                ..Default::default()
1243            }),
1244        }]);
1245
1246        let result = SolanaTransactionValidator::validate_token_transfers(
1247            &tx,
1248            &policy,
1249            &provider,
1250            &Pubkey::new_unique(),
1251        )
1252        .await;
1253
1254        match result {
1255            Err(SolanaTransactionValidationError::PolicyViolation(msg)) => {
1256                assert!(
1257                    msg.contains("not allowed for transfers"),
1258                    "Error message '{}' should contain 'not allowed for transfers'",
1259                    msg
1260                );
1261            }
1262            other => panic!("Expected PolicyViolation error, got {:?}", other),
1263        }
1264    }
1265}