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