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