Skip to main content

kora_lib/validator/
transaction_validator.rs

1use crate::{
2    config::FeePayerPolicy,
3    error::KoraError,
4    fee::fee::{FeeConfigUtil, TotalFeeCalculation},
5    oracle::PriceSource,
6    state::get_config,
7    token::{interface::TokenMint, token::TokenUtil},
8    transaction::{
9        ParsedSPLInstructionData, ParsedSPLInstructionType, ParsedSystemInstructionData,
10        ParsedSystemInstructionType, VersionedTransactionResolved,
11    },
12};
13use solana_client::nonblocking::rpc_client::RpcClient;
14use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction};
15use std::str::FromStr;
16
17use crate::fee::price::PriceModel;
18
19pub struct TransactionValidator {
20    fee_payer_pubkey: Pubkey,
21    max_allowed_lamports: u64,
22    allowed_programs: Vec<Pubkey>,
23    max_signatures: u64,
24    allowed_tokens: Vec<Pubkey>,
25    disallowed_accounts: Vec<Pubkey>,
26    _price_source: PriceSource,
27    fee_payer_policy: FeePayerPolicy,
28    allow_durable_transactions: bool,
29}
30
31impl TransactionValidator {
32    pub fn new(fee_payer_pubkey: Pubkey) -> Result<Self, KoraError> {
33        let config = &get_config()?.validation;
34
35        // Convert string program IDs to Pubkeys
36        let allowed_programs = config
37            .allowed_programs
38            .iter()
39            .map(|addr| {
40                Pubkey::from_str(addr).map_err(|e| {
41                    KoraError::InternalServerError(format!(
42                        "Invalid program address in config: {e}"
43                    ))
44                })
45            })
46            .collect::<Result<Vec<Pubkey>, KoraError>>()?;
47
48        Ok(Self {
49            fee_payer_pubkey,
50            max_allowed_lamports: config.max_allowed_lamports,
51            allowed_programs,
52            max_signatures: config.max_signatures,
53            _price_source: config.price_source.clone(),
54            allowed_tokens: config
55                .allowed_tokens
56                .iter()
57                .map(|addr| Pubkey::from_str(addr))
58                .collect::<Result<Vec<Pubkey>, _>>()
59                .map_err(|e| {
60                    KoraError::InternalServerError(format!("Invalid allowed token address: {e}"))
61                })?,
62            disallowed_accounts: config
63                .disallowed_accounts
64                .iter()
65                .map(|addr| Pubkey::from_str(addr))
66                .collect::<Result<Vec<Pubkey>, _>>()
67                .map_err(|e| {
68                    KoraError::InternalServerError(format!(
69                        "Invalid disallowed account address: {e}"
70                    ))
71                })?,
72            fee_payer_policy: config.fee_payer_policy.clone(),
73            allow_durable_transactions: config.allow_durable_transactions,
74        })
75    }
76
77    pub async fn fetch_and_validate_token_mint(
78        &self,
79        mint: &Pubkey,
80        rpc_client: &RpcClient,
81    ) -> Result<Box<dyn TokenMint + Send + Sync>, KoraError> {
82        // First check if the mint is in allowed tokens
83        if !self.allowed_tokens.contains(mint) {
84            return Err(KoraError::InvalidTransaction(format!(
85                "Mint {mint} is not a valid token mint"
86            )));
87        }
88
89        let mint = TokenUtil::get_mint(rpc_client, mint).await?;
90
91        Ok(mint)
92    }
93
94    /*
95    This function is used to validate a transaction.
96     */
97    pub async fn validate_transaction(
98        &self,
99        transaction_resolved: &mut VersionedTransactionResolved,
100        rpc_client: &RpcClient,
101    ) -> Result<(), KoraError> {
102        if transaction_resolved.all_instructions.is_empty() {
103            return Err(KoraError::InvalidTransaction(
104                "Transaction contains no instructions".to_string(),
105            ));
106        }
107
108        if transaction_resolved.all_account_keys.is_empty() {
109            return Err(KoraError::InvalidTransaction(
110                "Transaction contains no account keys".to_string(),
111            ));
112        }
113
114        self.validate_signatures(&transaction_resolved.transaction)?;
115
116        self.validate_programs(transaction_resolved)?;
117        self.validate_transfer_amounts(transaction_resolved, rpc_client).await?;
118        self.validate_disallowed_accounts(transaction_resolved)?;
119        self.validate_fee_payer_usage(transaction_resolved)?;
120
121        Ok(())
122    }
123
124    pub fn validate_lamport_fee(&self, fee: u64) -> Result<(), KoraError> {
125        if fee > self.max_allowed_lamports {
126            return Err(KoraError::InvalidTransaction(format!(
127                "Fee {} exceeds maximum allowed {}",
128                fee, self.max_allowed_lamports
129            )));
130        }
131        Ok(())
132    }
133
134    fn validate_signatures(&self, transaction: &VersionedTransaction) -> Result<(), KoraError> {
135        if transaction.signatures.len() > self.max_signatures as usize {
136            return Err(KoraError::InvalidTransaction(format!(
137                "Too many signatures: {} > {}",
138                transaction.signatures.len(),
139                self.max_signatures
140            )));
141        }
142
143        if transaction.signatures.is_empty() {
144            return Err(KoraError::InvalidTransaction("No signatures found".to_string()));
145        }
146
147        Ok(())
148    }
149
150    fn validate_programs(
151        &self,
152        transaction_resolved: &VersionedTransactionResolved,
153    ) -> Result<(), KoraError> {
154        for instruction in &transaction_resolved.all_instructions {
155            if !self.allowed_programs.contains(&instruction.program_id) {
156                return Err(KoraError::InvalidTransaction(format!(
157                    "Program {} is not in the allowed list",
158                    instruction.program_id
159                )));
160            }
161        }
162        Ok(())
163    }
164
165    fn validate_fee_payer_usage(
166        &self,
167        transaction_resolved: &mut VersionedTransactionResolved,
168    ) -> Result<(), KoraError> {
169        let system_instructions = transaction_resolved.get_or_parse_system_instructions()?;
170
171        // Check for durable transactions (nonce-based) - reject if not allowed
172        if !self.allow_durable_transactions
173            && system_instructions
174                .contains_key(&ParsedSystemInstructionType::SystemAdvanceNonceAccount)
175        {
176            return Err(KoraError::InvalidTransaction(
177                "Durable transactions (nonce-based) are not allowed".to_string(),
178            ));
179        }
180
181        // Validate system program instructions
182        validate_system!(self, system_instructions, SystemTransfer,
183            ParsedSystemInstructionData::SystemTransfer { sender, .. } => sender,
184            self.fee_payer_policy.system.allow_transfer, "System Transfer");
185
186        validate_system!(self, system_instructions, SystemAssign,
187            ParsedSystemInstructionData::SystemAssign { authority } => authority,
188            self.fee_payer_policy.system.allow_assign, "System Assign");
189
190        validate_system!(self, system_instructions, SystemAllocate,
191            ParsedSystemInstructionData::SystemAllocate { account } => account,
192            self.fee_payer_policy.system.allow_allocate, "System Allocate");
193
194        validate_system!(self, system_instructions, SystemCreateAccount,
195            ParsedSystemInstructionData::SystemCreateAccount { payer, .. } => payer,
196            self.fee_payer_policy.system.allow_create_account, "System Create Account");
197
198        validate_system!(self, system_instructions, SystemInitializeNonceAccount,
199            ParsedSystemInstructionData::SystemInitializeNonceAccount { nonce_authority, .. } => nonce_authority,
200            self.fee_payer_policy.system.nonce.allow_initialize, "System Initialize Nonce Account");
201
202        validate_system!(self, system_instructions, SystemAdvanceNonceAccount,
203            ParsedSystemInstructionData::SystemAdvanceNonceAccount { nonce_authority, .. } => nonce_authority,
204            self.fee_payer_policy.system.nonce.allow_advance, "System Advance Nonce Account");
205
206        validate_system!(self, system_instructions, SystemAuthorizeNonceAccount,
207            ParsedSystemInstructionData::SystemAuthorizeNonceAccount { nonce_authority, .. } => nonce_authority,
208            self.fee_payer_policy.system.nonce.allow_authorize, "System Authorize Nonce Account");
209
210        // Note: SystemUpgradeNonceAccount not validated - no authority parameter
211
212        validate_system!(self, system_instructions, SystemWithdrawNonceAccount,
213            ParsedSystemInstructionData::SystemWithdrawNonceAccount { nonce_authority, .. } => nonce_authority,
214            self.fee_payer_policy.system.nonce.allow_withdraw, "System Withdraw Nonce Account");
215
216        // Validate SPL instructions
217        let spl_instructions = transaction_resolved.get_or_parse_spl_instructions()?;
218
219        validate_spl!(self, spl_instructions, SplTokenTransfer,
220            ParsedSPLInstructionData::SplTokenTransfer { owner, is_2022, .. } => { owner, is_2022 },
221            self.fee_payer_policy.spl_token.allow_transfer,
222            self.fee_payer_policy.token_2022.allow_transfer,
223            "SPL Token Transfer", "Token2022 Token Transfer");
224
225        validate_spl!(self, spl_instructions, SplTokenApprove,
226            ParsedSPLInstructionData::SplTokenApprove { owner, is_2022, .. } => { owner, is_2022 },
227            self.fee_payer_policy.spl_token.allow_approve,
228            self.fee_payer_policy.token_2022.allow_approve,
229            "SPL Token Approve", "Token2022 Token Approve");
230
231        validate_spl!(self, spl_instructions, SplTokenBurn,
232            ParsedSPLInstructionData::SplTokenBurn { owner, is_2022 } => { owner, is_2022 },
233            self.fee_payer_policy.spl_token.allow_burn,
234            self.fee_payer_policy.token_2022.allow_burn,
235            "SPL Token Burn", "Token2022 Token Burn");
236
237        validate_spl!(self, spl_instructions, SplTokenCloseAccount,
238            ParsedSPLInstructionData::SplTokenCloseAccount { owner, is_2022 } => { owner, is_2022 },
239            self.fee_payer_policy.spl_token.allow_close_account,
240            self.fee_payer_policy.token_2022.allow_close_account,
241            "SPL Token Close Account", "Token2022 Token Close Account");
242
243        validate_spl!(self, spl_instructions, SplTokenRevoke,
244            ParsedSPLInstructionData::SplTokenRevoke { owner, is_2022 } => { owner, is_2022 },
245            self.fee_payer_policy.spl_token.allow_revoke,
246            self.fee_payer_policy.token_2022.allow_revoke,
247            "SPL Token Revoke", "Token2022 Token Revoke");
248
249        validate_spl!(self, spl_instructions, SplTokenSetAuthority,
250            ParsedSPLInstructionData::SplTokenSetAuthority { authority, is_2022 } => { authority, is_2022 },
251            self.fee_payer_policy.spl_token.allow_set_authority,
252            self.fee_payer_policy.token_2022.allow_set_authority,
253            "SPL Token SetAuthority", "Token2022 Token SetAuthority");
254
255        validate_spl!(self, spl_instructions, SplTokenMintTo,
256            ParsedSPLInstructionData::SplTokenMintTo { mint_authority, is_2022 } => { mint_authority, is_2022 },
257            self.fee_payer_policy.spl_token.allow_mint_to,
258            self.fee_payer_policy.token_2022.allow_mint_to,
259            "SPL Token MintTo", "Token2022 Token MintTo");
260
261        validate_spl!(self, spl_instructions, SplTokenInitializeMint,
262            ParsedSPLInstructionData::SplTokenInitializeMint { mint_authority, is_2022 } => { mint_authority, is_2022 },
263            self.fee_payer_policy.spl_token.allow_initialize_mint,
264            self.fee_payer_policy.token_2022.allow_initialize_mint,
265            "SPL Token InitializeMint", "Token2022 Token InitializeMint");
266
267        validate_spl!(self, spl_instructions, SplTokenInitializeAccount,
268            ParsedSPLInstructionData::SplTokenInitializeAccount { owner, is_2022 } => { owner, is_2022 },
269            self.fee_payer_policy.spl_token.allow_initialize_account,
270            self.fee_payer_policy.token_2022.allow_initialize_account,
271            "SPL Token InitializeAccount", "Token2022 Token InitializeAccount");
272
273        validate_spl_multisig!(self, spl_instructions, SplTokenInitializeMultisig,
274            ParsedSPLInstructionData::SplTokenInitializeMultisig { signers, is_2022 } => { signers, is_2022 },
275            self.fee_payer_policy.spl_token.allow_initialize_multisig,
276            self.fee_payer_policy.token_2022.allow_initialize_multisig,
277            "SPL Token InitializeMultisig", "Token2022 Token InitializeMultisig");
278
279        validate_spl!(self, spl_instructions, SplTokenFreezeAccount,
280            ParsedSPLInstructionData::SplTokenFreezeAccount { freeze_authority, is_2022 } => { freeze_authority, is_2022 },
281            self.fee_payer_policy.spl_token.allow_freeze_account,
282            self.fee_payer_policy.token_2022.allow_freeze_account,
283            "SPL Token FreezeAccount", "Token2022 Token FreezeAccount");
284
285        validate_spl!(self, spl_instructions, SplTokenThawAccount,
286            ParsedSPLInstructionData::SplTokenThawAccount { freeze_authority, is_2022 } => { freeze_authority, is_2022 },
287            self.fee_payer_policy.spl_token.allow_thaw_account,
288            self.fee_payer_policy.token_2022.allow_thaw_account,
289            "SPL Token ThawAccount", "Token2022 Token ThawAccount");
290
291        Ok(())
292    }
293
294    async fn validate_transfer_amounts(
295        &self,
296        transaction_resolved: &mut VersionedTransactionResolved,
297        rpc_client: &RpcClient,
298    ) -> Result<(), KoraError> {
299        let total_outflow = self.calculate_total_outflow(transaction_resolved, rpc_client).await?;
300
301        if total_outflow > self.max_allowed_lamports {
302            return Err(KoraError::InvalidTransaction(format!(
303                "Total transfer amount {} exceeds maximum allowed {}",
304                total_outflow, self.max_allowed_lamports
305            )));
306        }
307
308        Ok(())
309    }
310
311    fn validate_disallowed_accounts(
312        &self,
313        transaction_resolved: &VersionedTransactionResolved,
314    ) -> Result<(), KoraError> {
315        for instruction in &transaction_resolved.all_instructions {
316            if self.disallowed_accounts.contains(&instruction.program_id) {
317                return Err(KoraError::InvalidTransaction(format!(
318                    "Program {} is disallowed",
319                    instruction.program_id
320                )));
321            }
322
323            for account_index in instruction.accounts.iter() {
324                if self.disallowed_accounts.contains(&account_index.pubkey) {
325                    return Err(KoraError::InvalidTransaction(format!(
326                        "Account {} is disallowed",
327                        account_index.pubkey
328                    )));
329                }
330            }
331        }
332        Ok(())
333    }
334
335    pub fn is_disallowed_account(&self, account: &Pubkey) -> bool {
336        self.disallowed_accounts.contains(account)
337    }
338
339    async fn calculate_total_outflow(
340        &self,
341        transaction_resolved: &mut VersionedTransactionResolved,
342        rpc_client: &RpcClient,
343    ) -> Result<u64, KoraError> {
344        let config = get_config()?;
345        FeeConfigUtil::calculate_fee_payer_outflow(
346            &self.fee_payer_pubkey,
347            transaction_resolved,
348            rpc_client,
349            &config.validation.price_source,
350        )
351        .await
352    }
353
354    pub async fn validate_token_payment(
355        transaction_resolved: &mut VersionedTransactionResolved,
356        required_lamports: u64,
357        rpc_client: &RpcClient,
358        expected_payment_destination: &Pubkey,
359    ) -> Result<(), KoraError> {
360        if TokenUtil::verify_token_payment(
361            transaction_resolved,
362            rpc_client,
363            required_lamports,
364            expected_payment_destination,
365        )
366        .await?
367        {
368            return Ok(());
369        }
370
371        Err(KoraError::InvalidTransaction(format!(
372            "Insufficient token payment. Required {required_lamports} lamports"
373        )))
374    }
375
376    pub fn validate_strict_pricing_with_fee(
377        fee_calculation: &TotalFeeCalculation,
378    ) -> Result<(), KoraError> {
379        let config = get_config()?;
380
381        if !matches!(&config.validation.price.model, PriceModel::Fixed { strict: true, .. }) {
382            return Ok(());
383        }
384
385        let fixed_price_lamports = fee_calculation.total_fee_lamports;
386        let total_fee_lamports = fee_calculation.get_total_fee_lamports()?;
387
388        if fixed_price_lamports < total_fee_lamports {
389            log::error!(
390                "Strict pricing violation: fixed_price_lamports={} < total_fee_lamports={}",
391                fixed_price_lamports,
392                total_fee_lamports
393            );
394            return Err(KoraError::ValidationError(format!(
395                    "Strict pricing violation: total fee ({} lamports) exceeds fixed price ({} lamports)",
396                    total_fee_lamports,
397                    fixed_price_lamports
398                )));
399        }
400
401        Ok(())
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use crate::{
408        config::FeePayerPolicy,
409        state::update_config,
410        tests::{config_mock::ConfigMockBuilder, rpc_mock::RpcMockBuilder},
411        transaction::TransactionUtil,
412    };
413    use serial_test::serial;
414
415    use super::*;
416    use solana_message::{Message, VersionedMessage};
417    use solana_sdk::instruction::Instruction;
418    use solana_system_interface::{
419        instruction::{
420            assign, create_account, create_account_with_seed, transfer, transfer_with_seed,
421        },
422        program::ID as SYSTEM_PROGRAM_ID,
423    };
424
425    // Helper functions to reduce test duplication and setup config
426    fn setup_default_config() {
427        let config = ConfigMockBuilder::new()
428            .with_price_source(PriceSource::Mock)
429            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
430            .with_max_allowed_lamports(1_000_000)
431            .with_fee_payer_policy(FeePayerPolicy::default())
432            .build();
433        update_config(config).unwrap();
434    }
435
436    fn setup_config_with_policy(policy: FeePayerPolicy) {
437        let config = ConfigMockBuilder::new()
438            .with_price_source(PriceSource::Mock)
439            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
440            .with_max_allowed_lamports(1_000_000)
441            .with_fee_payer_policy(policy)
442            .build();
443        update_config(config).unwrap();
444    }
445
446    fn setup_spl_config_with_policy(policy: FeePayerPolicy) {
447        let config = ConfigMockBuilder::new()
448            .with_price_source(PriceSource::Mock)
449            .with_allowed_programs(vec![spl_token_interface::id().to_string()])
450            .with_max_allowed_lamports(1_000_000)
451            .with_fee_payer_policy(policy)
452            .build();
453        update_config(config).unwrap();
454    }
455
456    fn setup_token2022_config_with_policy(policy: FeePayerPolicy) {
457        let config = ConfigMockBuilder::new()
458            .with_price_source(PriceSource::Mock)
459            .with_allowed_programs(vec![spl_token_2022_interface::id().to_string()])
460            .with_max_allowed_lamports(1_000_000)
461            .with_fee_payer_policy(policy)
462            .build();
463        update_config(config).unwrap();
464    }
465
466    #[tokio::test]
467    #[serial]
468    async fn test_validate_transaction() {
469        let fee_payer = Pubkey::new_unique();
470        setup_default_config();
471        let rpc_client = RpcMockBuilder::new().build();
472
473        let validator = TransactionValidator::new(fee_payer).unwrap();
474
475        let recipient = Pubkey::new_unique();
476        let sender = Pubkey::new_unique();
477        let instruction = transfer(&sender, &recipient, 100_000);
478        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
479        let mut transaction =
480            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
481
482        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
483    }
484
485    #[tokio::test]
486    #[serial]
487    async fn test_transfer_amount_limits() {
488        let fee_payer = Pubkey::new_unique();
489        setup_default_config();
490        let rpc_client = RpcMockBuilder::new().build();
491
492        let validator = TransactionValidator::new(fee_payer).unwrap();
493        let sender = Pubkey::new_unique();
494        let recipient = Pubkey::new_unique();
495
496        // Test transaction with amount over limit
497        let instruction = transfer(&sender, &recipient, 2_000_000);
498        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
499        let mut transaction =
500            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
501
502        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
503
504        // Test multiple transfers
505        let instructions =
506            vec![transfer(&sender, &recipient, 500_000), transfer(&sender, &recipient, 500_000)];
507        let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
508        let mut transaction =
509            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
510        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
511    }
512
513    #[tokio::test]
514    #[serial]
515    async fn test_validate_programs() {
516        let fee_payer = Pubkey::new_unique();
517        setup_default_config();
518        let rpc_client = RpcMockBuilder::new().build();
519
520        let validator = TransactionValidator::new(fee_payer).unwrap();
521        let sender = Pubkey::new_unique();
522        let recipient = Pubkey::new_unique();
523
524        // Test allowed program (system program)
525        let instruction = transfer(&sender, &recipient, 1000);
526        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
527        let mut transaction =
528            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
529        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
530
531        // Test disallowed program
532        let fake_program = Pubkey::new_unique();
533        // Create a no-op instruction for the fake program
534        let instruction = Instruction::new_with_bincode(
535            fake_program,
536            &[0u8],
537            vec![], // no accounts needed for this test
538        );
539        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
540        let mut transaction =
541            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
542        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
543    }
544
545    #[tokio::test]
546    #[serial]
547    async fn test_validate_signatures() {
548        let fee_payer = Pubkey::new_unique();
549        let config = ConfigMockBuilder::new()
550            .with_price_source(PriceSource::Mock)
551            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
552            .with_max_allowed_lamports(1_000_000)
553            .with_max_signatures(2)
554            .with_fee_payer_policy(FeePayerPolicy::default())
555            .build();
556        update_config(config).unwrap();
557
558        let rpc_client = RpcMockBuilder::new().build();
559        let validator = TransactionValidator::new(fee_payer).unwrap();
560        let sender = Pubkey::new_unique();
561        let recipient = Pubkey::new_unique();
562
563        // Test too many signatures
564        let instructions = vec![
565            transfer(&sender, &recipient, 1000),
566            transfer(&sender, &recipient, 1000),
567            transfer(&sender, &recipient, 1000),
568        ];
569        let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
570        let mut transaction =
571            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
572        transaction.transaction.signatures = vec![Default::default(); 3]; // Add 3 dummy signatures
573        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
574    }
575
576    #[tokio::test]
577    #[serial]
578    async fn test_sign_and_send_transaction_mode() {
579        let fee_payer = Pubkey::new_unique();
580        setup_default_config();
581        let rpc_client = RpcMockBuilder::new().build();
582
583        let validator = TransactionValidator::new(fee_payer).unwrap();
584        let sender = Pubkey::new_unique();
585        let recipient = Pubkey::new_unique();
586
587        // Test SignAndSend mode with fee payer already set should not error
588        let instruction = transfer(&sender, &recipient, 1000);
589        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
590        let mut transaction =
591            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
592        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
593
594        // Test SignAndSend mode without fee payer (should succeed)
595        let instruction = transfer(&sender, &recipient, 1000);
596        let message = VersionedMessage::Legacy(Message::new(&[instruction], None)); // No fee payer specified
597        let mut transaction =
598            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
599        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
600    }
601
602    #[tokio::test]
603    #[serial]
604    async fn test_empty_transaction() {
605        let fee_payer = Pubkey::new_unique();
606        setup_default_config();
607        let rpc_client = RpcMockBuilder::new().build();
608
609        let validator = TransactionValidator::new(fee_payer).unwrap();
610
611        // Create an empty message using Message::new with empty instructions
612        let message = VersionedMessage::Legacy(Message::new(&[], Some(&fee_payer)));
613        let mut transaction =
614            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
615        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
616    }
617
618    #[tokio::test]
619    #[serial]
620    async fn test_disallowed_accounts() {
621        let fee_payer = Pubkey::new_unique();
622        let config = ConfigMockBuilder::new()
623            .with_price_source(PriceSource::Mock)
624            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
625            .with_max_allowed_lamports(1_000_000)
626            .with_disallowed_accounts(vec![
627                "hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek".to_string()
628            ])
629            .with_fee_payer_policy(FeePayerPolicy::default())
630            .build();
631        update_config(config).unwrap();
632
633        let rpc_client = RpcMockBuilder::new().build();
634        let validator = TransactionValidator::new(fee_payer).unwrap();
635        let instruction = transfer(
636            &Pubkey::from_str("hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek").unwrap(),
637            &fee_payer,
638            1000,
639        );
640        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
641        let mut transaction =
642            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
643        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
644    }
645
646    #[tokio::test]
647    #[serial]
648    async fn test_fee_payer_policy_sol_transfers() {
649        let fee_payer = Pubkey::new_unique();
650        let recipient = Pubkey::new_unique();
651
652        // Test with allow_sol_transfers = true
653        let rpc_client = RpcMockBuilder::new().build();
654        let mut policy = FeePayerPolicy::default();
655        policy.system.allow_transfer = true;
656        setup_config_with_policy(policy);
657
658        let validator = TransactionValidator::new(fee_payer).unwrap();
659
660        let instruction = transfer(&fee_payer, &recipient, 1000);
661
662        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
663        let mut transaction =
664            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
665        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
666
667        // Test with allow_sol_transfers = false
668        let rpc_client = RpcMockBuilder::new().build();
669        let mut policy = FeePayerPolicy::default();
670        policy.system.allow_transfer = false;
671        setup_config_with_policy(policy);
672
673        let validator = TransactionValidator::new(fee_payer).unwrap();
674
675        let instruction = transfer(&fee_payer, &recipient, 1000);
676        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
677        let mut transaction =
678            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
679        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
680    }
681
682    #[tokio::test]
683    #[serial]
684    async fn test_fee_payer_policy_assign() {
685        let fee_payer = Pubkey::new_unique();
686        let new_owner = Pubkey::new_unique();
687
688        // Test with allow_assign = true
689
690        let rpc_client = RpcMockBuilder::new().build();
691
692        let mut policy = FeePayerPolicy::default();
693        policy.system.allow_assign = true;
694        setup_config_with_policy(policy);
695
696        let validator = TransactionValidator::new(fee_payer).unwrap();
697
698        let instruction = assign(&fee_payer, &new_owner);
699        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
700        let mut transaction =
701            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
702        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
703
704        // Test with allow_assign = false
705
706        let rpc_client = RpcMockBuilder::new().build();
707
708        let mut policy = FeePayerPolicy::default();
709        policy.system.allow_assign = false;
710        setup_config_with_policy(policy);
711
712        let validator = TransactionValidator::new(fee_payer).unwrap();
713
714        let instruction = assign(&fee_payer, &new_owner);
715        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
716        let mut transaction =
717            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
718        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
719    }
720
721    #[tokio::test]
722    #[serial]
723    async fn test_fee_payer_policy_spl_transfers() {
724        let fee_payer = Pubkey::new_unique();
725
726        let fee_payer_token_account = Pubkey::new_unique();
727        let recipient_token_account = Pubkey::new_unique();
728
729        // Test with allow_spl_transfers = true
730        let rpc_client = RpcMockBuilder::new().build();
731
732        let mut policy = FeePayerPolicy::default();
733        policy.spl_token.allow_transfer = true;
734        setup_spl_config_with_policy(policy);
735
736        let validator = TransactionValidator::new(fee_payer).unwrap();
737
738        let transfer_ix = spl_token_interface::instruction::transfer(
739            &spl_token_interface::id(),
740            &fee_payer_token_account,
741            &recipient_token_account,
742            &fee_payer, // fee payer is the signer
743            &[],
744            1000,
745        )
746        .unwrap();
747
748        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
749        let mut transaction =
750            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
751        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
752
753        // Test with allow_spl_transfers = false
754        let rpc_client = RpcMockBuilder::new().build();
755
756        let mut policy = FeePayerPolicy::default();
757        policy.spl_token.allow_transfer = false;
758        setup_spl_config_with_policy(policy);
759
760        let validator = TransactionValidator::new(fee_payer).unwrap();
761
762        let transfer_ix = spl_token_interface::instruction::transfer(
763            &spl_token_interface::id(),
764            &fee_payer_token_account,
765            &recipient_token_account,
766            &fee_payer, // fee payer is the signer
767            &[],
768            1000,
769        )
770        .unwrap();
771
772        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
773        let mut transaction =
774            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
775        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
776
777        // Test with other account as source - should always pass
778        let other_signer = Pubkey::new_unique();
779        let transfer_ix = spl_token_interface::instruction::transfer(
780            &spl_token_interface::id(),
781            &fee_payer_token_account,
782            &recipient_token_account,
783            &other_signer, // other account is the signer
784            &[],
785            1000,
786        )
787        .unwrap();
788
789        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
790        let mut transaction =
791            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
792        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
793    }
794
795    #[tokio::test]
796    #[serial]
797    async fn test_fee_payer_policy_token2022_transfers() {
798        let fee_payer = Pubkey::new_unique();
799
800        let fee_payer_token_account = Pubkey::new_unique();
801        let recipient_token_account = Pubkey::new_unique();
802        let mint = Pubkey::new_unique();
803
804        // Test with allow_token2022_transfers = true
805        let rpc_client = RpcMockBuilder::new()
806            .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation
807            .build();
808        // Test with token_2022.allow_transfer = true
809        let mut policy = FeePayerPolicy::default();
810        policy.token_2022.allow_transfer = true;
811        setup_token2022_config_with_policy(policy);
812
813        let validator = TransactionValidator::new(fee_payer).unwrap();
814
815        let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
816            &spl_token_2022_interface::id(),
817            &fee_payer_token_account,
818            &mint,
819            &recipient_token_account,
820            &fee_payer, // fee payer is the signer
821            &[],
822            1,
823            2,
824        )
825        .unwrap();
826
827        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
828        let mut transaction =
829            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
830        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
831
832        // Test with allow_token2022_transfers = false
833        let rpc_client = RpcMockBuilder::new()
834            .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation
835            .build();
836        let mut policy = FeePayerPolicy::default();
837        policy.token_2022.allow_transfer = false;
838        setup_token2022_config_with_policy(policy);
839
840        let validator = TransactionValidator::new(fee_payer).unwrap();
841
842        let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
843            &spl_token_2022_interface::id(),
844            &fee_payer_token_account,
845            &mint,
846            &recipient_token_account,
847            &fee_payer, // fee payer is the signer
848            &[],
849            1000,
850            2,
851        )
852        .unwrap();
853
854        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
855        let mut transaction =
856            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
857
858        // Should fail because fee payer is not allowed to be source
859        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
860
861        // Test with other account as source - should always pass
862        let other_signer = Pubkey::new_unique();
863        let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
864            &spl_token_2022_interface::id(),
865            &fee_payer_token_account,
866            &mint,
867            &recipient_token_account,
868            &other_signer, // other account is the signer
869            &[],
870            1000,
871            2,
872        )
873        .unwrap();
874
875        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
876        let mut transaction =
877            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
878
879        // Should pass because fee payer is not the source
880        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
881    }
882
883    #[tokio::test]
884    #[serial]
885    async fn test_calculate_total_outflow() {
886        let fee_payer = Pubkey::new_unique();
887        let config = ConfigMockBuilder::new()
888            .with_price_source(PriceSource::Mock)
889            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
890            .with_max_allowed_lamports(10_000_000)
891            .with_fee_payer_policy(FeePayerPolicy::default())
892            .build();
893        update_config(config).unwrap();
894
895        let rpc_client = RpcMockBuilder::new().build();
896        let validator = TransactionValidator::new(fee_payer).unwrap();
897
898        // Test 1: Fee payer as sender in Transfer - should add to outflow
899        let recipient = Pubkey::new_unique();
900        let transfer_instruction = transfer(&fee_payer, &recipient, 100_000);
901        let message =
902            VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
903        let mut transaction =
904            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
905        let outflow =
906            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
907        assert_eq!(outflow, 100_000, "Transfer from fee payer should add to outflow");
908
909        // Test 2: Fee payer as recipient in Transfer - should subtract from outflow (account closure)
910        let sender = Pubkey::new_unique();
911        let transfer_instruction = transfer(&sender, &fee_payer, 50_000);
912        let message =
913            VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
914        let mut transaction =
915            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
916
917        let outflow =
918            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
919        assert_eq!(outflow, 0, "Transfer to fee payer should subtract from outflow"); // 0 - 50_000 = 0 (saturating_sub)
920
921        // Test 3: Fee payer as funding account in CreateAccount - should add to outflow
922        let new_account = Pubkey::new_unique();
923        let create_instruction = create_account(
924            &fee_payer,
925            &new_account,
926            200_000, // lamports
927            100,     // space
928            &SYSTEM_PROGRAM_ID,
929        );
930        let message =
931            VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer)));
932        let mut transaction =
933            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
934        let outflow =
935            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
936        assert_eq!(outflow, 200_000, "CreateAccount funded by fee payer should add to outflow");
937
938        // Test 4: Fee payer as funding account in CreateAccountWithSeed - should add to outflow
939        let create_with_seed_instruction = create_account_with_seed(
940            &fee_payer,
941            &new_account,
942            &fee_payer,
943            "test_seed",
944            300_000, // lamports
945            100,     // space
946            &SYSTEM_PROGRAM_ID,
947        );
948        let message = VersionedMessage::Legacy(Message::new(
949            &[create_with_seed_instruction],
950            Some(&fee_payer),
951        ));
952        let mut transaction =
953            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
954        let outflow =
955            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
956        assert_eq!(
957            outflow, 300_000,
958            "CreateAccountWithSeed funded by fee payer should add to outflow"
959        );
960
961        // Test 5: TransferWithSeed from fee payer - should add to outflow
962        let transfer_with_seed_instruction = transfer_with_seed(
963            &fee_payer,
964            &fee_payer,
965            "test_seed".to_string(),
966            &SYSTEM_PROGRAM_ID,
967            &recipient,
968            150_000,
969        );
970        let message = VersionedMessage::Legacy(Message::new(
971            &[transfer_with_seed_instruction],
972            Some(&fee_payer),
973        ));
974        let mut transaction =
975            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
976        let outflow =
977            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
978        assert_eq!(outflow, 150_000, "TransferWithSeed from fee payer should add to outflow");
979
980        // Test 6: Multiple instructions - should sum correctly
981        let instructions = vec![
982            transfer(&fee_payer, &recipient, 100_000), // +100_000
983            transfer(&sender, &fee_payer, 30_000),     // -30_000
984            create_account(&fee_payer, &new_account, 50_000, 100, &SYSTEM_PROGRAM_ID), // +50_000
985        ];
986        let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
987        let mut transaction =
988            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
989        let outflow =
990            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
991        assert_eq!(
992            outflow, 120_000,
993            "Multiple instructions should sum correctly: 100000 - 30000 + 50000 = 120000"
994        );
995
996        // Test 7: Other account as sender - should not affect outflow
997        let other_sender = Pubkey::new_unique();
998        let transfer_instruction = transfer(&other_sender, &recipient, 500_000);
999        let message =
1000            VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
1001        let mut transaction =
1002            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1003        let outflow =
1004            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
1005        assert_eq!(outflow, 0, "Transfer from other account should not affect outflow");
1006
1007        // Test 8: Other account funding CreateAccount - should not affect outflow
1008        let other_funder = Pubkey::new_unique();
1009        let create_instruction =
1010            create_account(&other_funder, &new_account, 1_000_000, 100, &SYSTEM_PROGRAM_ID);
1011        let message =
1012            VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer)));
1013        let mut transaction =
1014            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1015        let outflow =
1016            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
1017        assert_eq!(outflow, 0, "CreateAccount funded by other account should not affect outflow");
1018    }
1019
1020    #[tokio::test]
1021    #[serial]
1022    async fn test_fee_payer_policy_burn() {
1023        let fee_payer = Pubkey::new_unique();
1024        let fee_payer_token_account = Pubkey::new_unique();
1025        let mint = Pubkey::new_unique();
1026
1027        // Test with allow_burn = true
1028
1029        let rpc_client = RpcMockBuilder::new().build();
1030        let mut policy = FeePayerPolicy::default();
1031        policy.spl_token.allow_burn = true;
1032        setup_spl_config_with_policy(policy);
1033
1034        let validator = TransactionValidator::new(fee_payer).unwrap();
1035
1036        let burn_ix = spl_token_interface::instruction::burn(
1037            &spl_token_interface::id(),
1038            &fee_payer_token_account,
1039            &mint,
1040            &fee_payer,
1041            &[],
1042            1000,
1043        )
1044        .unwrap();
1045
1046        let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer)));
1047        let mut transaction =
1048            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1049        // Should pass because allow_burn is true by default
1050        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1051
1052        // Test with allow_burn = false
1053
1054        let rpc_client = RpcMockBuilder::new().build();
1055        let mut policy = FeePayerPolicy::default();
1056        policy.spl_token.allow_burn = false;
1057        setup_spl_config_with_policy(policy);
1058
1059        let validator = TransactionValidator::new(fee_payer).unwrap();
1060
1061        let burn_ix = spl_token_interface::instruction::burn(
1062            &spl_token_interface::id(),
1063            &fee_payer_token_account,
1064            &mint,
1065            &fee_payer,
1066            &[],
1067            1000,
1068        )
1069        .unwrap();
1070
1071        let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer)));
1072        let mut transaction =
1073            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1074
1075        // Should fail because fee payer cannot burn tokens when allow_burn is false
1076        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1077
1078        // Test burn_checked instruction
1079        let burn_checked_ix = spl_token_interface::instruction::burn_checked(
1080            &spl_token_interface::id(),
1081            &fee_payer_token_account,
1082            &mint,
1083            &fee_payer,
1084            &[],
1085            1000,
1086            2,
1087        )
1088        .unwrap();
1089
1090        let message = VersionedMessage::Legacy(Message::new(&[burn_checked_ix], Some(&fee_payer)));
1091        let mut transaction =
1092            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1093
1094        // Should also fail for burn_checked
1095        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1096    }
1097
1098    #[tokio::test]
1099    #[serial]
1100    async fn test_fee_payer_policy_close_account() {
1101        let fee_payer = Pubkey::new_unique();
1102        let fee_payer_token_account = Pubkey::new_unique();
1103        let destination = Pubkey::new_unique();
1104
1105        // Test with allow_close_account = true
1106
1107        let rpc_client = RpcMockBuilder::new().build();
1108        let mut policy = FeePayerPolicy::default();
1109        policy.spl_token.allow_close_account = true;
1110        setup_spl_config_with_policy(policy);
1111
1112        let validator = TransactionValidator::new(fee_payer).unwrap();
1113
1114        let close_ix = spl_token_interface::instruction::close_account(
1115            &spl_token_interface::id(),
1116            &fee_payer_token_account,
1117            &destination,
1118            &fee_payer,
1119            &[],
1120        )
1121        .unwrap();
1122
1123        let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer)));
1124        let mut transaction =
1125            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1126        // Should pass because allow_close_account is true by default
1127        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1128
1129        // Test with allow_close_account = false
1130        let rpc_client = RpcMockBuilder::new().build();
1131        let mut policy = FeePayerPolicy::default();
1132        policy.spl_token.allow_close_account = false;
1133        setup_spl_config_with_policy(policy);
1134
1135        let validator = TransactionValidator::new(fee_payer).unwrap();
1136
1137        let close_ix = spl_token_interface::instruction::close_account(
1138            &spl_token_interface::id(),
1139            &fee_payer_token_account,
1140            &destination,
1141            &fee_payer,
1142            &[],
1143        )
1144        .unwrap();
1145
1146        let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer)));
1147        let mut transaction =
1148            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1149
1150        // Should fail because fee payer cannot close accounts when allow_close_account is false
1151        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1152    }
1153
1154    #[tokio::test]
1155    #[serial]
1156    async fn test_fee_payer_policy_approve() {
1157        let fee_payer = Pubkey::new_unique();
1158        let fee_payer_token_account = Pubkey::new_unique();
1159        let delegate = Pubkey::new_unique();
1160
1161        // Test with allow_approve = true
1162
1163        let rpc_client = RpcMockBuilder::new().build();
1164        let mut policy = FeePayerPolicy::default();
1165        policy.spl_token.allow_approve = true;
1166        setup_spl_config_with_policy(policy);
1167
1168        let validator = TransactionValidator::new(fee_payer).unwrap();
1169
1170        let approve_ix = spl_token_interface::instruction::approve(
1171            &spl_token_interface::id(),
1172            &fee_payer_token_account,
1173            &delegate,
1174            &fee_payer,
1175            &[],
1176            1000,
1177        )
1178        .unwrap();
1179
1180        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1181        let mut transaction =
1182            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1183        // Should pass because allow_approve is true by default
1184        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1185
1186        // Test with allow_approve = false
1187        let rpc_client = RpcMockBuilder::new().build();
1188        let mut policy = FeePayerPolicy::default();
1189        policy.spl_token.allow_approve = false;
1190        setup_spl_config_with_policy(policy);
1191
1192        let validator = TransactionValidator::new(fee_payer).unwrap();
1193
1194        let approve_ix = spl_token_interface::instruction::approve(
1195            &spl_token_interface::id(),
1196            &fee_payer_token_account,
1197            &delegate,
1198            &fee_payer,
1199            &[],
1200            1000,
1201        )
1202        .unwrap();
1203
1204        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1205        let mut transaction =
1206            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1207
1208        // Should fail because fee payer cannot approve when allow_approve is false
1209        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1210
1211        // Test approve_checked instruction
1212        let mint = Pubkey::new_unique();
1213        let approve_checked_ix = spl_token_interface::instruction::approve_checked(
1214            &spl_token_interface::id(),
1215            &fee_payer_token_account,
1216            &mint,
1217            &delegate,
1218            &fee_payer,
1219            &[],
1220            1000,
1221            2,
1222        )
1223        .unwrap();
1224
1225        let message =
1226            VersionedMessage::Legacy(Message::new(&[approve_checked_ix], Some(&fee_payer)));
1227        let mut transaction =
1228            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1229
1230        // Should also fail for approve_checked
1231        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1232    }
1233
1234    #[tokio::test]
1235    #[serial]
1236    async fn test_fee_payer_policy_token2022_burn() {
1237        let fee_payer = Pubkey::new_unique();
1238        let fee_payer_token_account = Pubkey::new_unique();
1239        let mint = Pubkey::new_unique();
1240
1241        // Test with allow_burn = false for Token2022
1242
1243        let rpc_client = RpcMockBuilder::new().build();
1244        let mut policy = FeePayerPolicy::default();
1245        policy.token_2022.allow_burn = false;
1246        setup_token2022_config_with_policy(policy);
1247
1248        let validator = TransactionValidator::new(fee_payer).unwrap();
1249
1250        let burn_ix = spl_token_2022_interface::instruction::burn(
1251            &spl_token_2022_interface::id(),
1252            &fee_payer_token_account,
1253            &mint,
1254            &fee_payer,
1255            &[],
1256            1000,
1257        )
1258        .unwrap();
1259
1260        let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer)));
1261        let mut transaction =
1262            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1263        // Should fail for Token2022 burn
1264        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1265    }
1266
1267    #[tokio::test]
1268    #[serial]
1269    async fn test_fee_payer_policy_token2022_close_account() {
1270        let fee_payer = Pubkey::new_unique();
1271        let fee_payer_token_account = Pubkey::new_unique();
1272        let destination = Pubkey::new_unique();
1273
1274        // Test with allow_close_account = false for Token2022
1275
1276        let rpc_client = RpcMockBuilder::new().build();
1277        let mut policy = FeePayerPolicy::default();
1278        policy.token_2022.allow_close_account = false;
1279        setup_token2022_config_with_policy(policy);
1280
1281        let validator = TransactionValidator::new(fee_payer).unwrap();
1282
1283        let close_ix = spl_token_2022_interface::instruction::close_account(
1284            &spl_token_2022_interface::id(),
1285            &fee_payer_token_account,
1286            &destination,
1287            &fee_payer,
1288            &[],
1289        )
1290        .unwrap();
1291
1292        let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer)));
1293        let mut transaction =
1294            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1295        // Should fail for Token2022 close account
1296        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1297    }
1298
1299    #[tokio::test]
1300    #[serial]
1301    async fn test_fee_payer_policy_token2022_approve() {
1302        let fee_payer = Pubkey::new_unique();
1303        let fee_payer_token_account = Pubkey::new_unique();
1304        let delegate = Pubkey::new_unique();
1305
1306        // Test with allow_approve = true
1307
1308        let rpc_client = RpcMockBuilder::new().build();
1309        let mut policy = FeePayerPolicy::default();
1310        policy.token_2022.allow_approve = true;
1311        setup_token2022_config_with_policy(policy);
1312
1313        let validator = TransactionValidator::new(fee_payer).unwrap();
1314
1315        let approve_ix = spl_token_2022_interface::instruction::approve(
1316            &spl_token_2022_interface::id(),
1317            &fee_payer_token_account,
1318            &delegate,
1319            &fee_payer,
1320            &[],
1321            1000,
1322        )
1323        .unwrap();
1324
1325        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1326        let mut transaction =
1327            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1328        // Should pass because allow_approve is true by default
1329        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1330
1331        // Test with allow_approve = false
1332
1333        let rpc_client = RpcMockBuilder::new().build();
1334        let mut policy = FeePayerPolicy::default();
1335        policy.token_2022.allow_approve = false;
1336        setup_token2022_config_with_policy(policy);
1337
1338        let validator = TransactionValidator::new(fee_payer).unwrap();
1339
1340        let approve_ix = spl_token_2022_interface::instruction::approve(
1341            &spl_token_2022_interface::id(),
1342            &fee_payer_token_account,
1343            &delegate,
1344            &fee_payer,
1345            &[],
1346            1000,
1347        )
1348        .unwrap();
1349
1350        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1351        let mut transaction =
1352            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1353
1354        // Should fail because fee payer cannot approve when allow_approve is false
1355        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1356
1357        // Test approve_checked instruction
1358        let mint = Pubkey::new_unique();
1359        let approve_checked_ix = spl_token_2022_interface::instruction::approve_checked(
1360            &spl_token_2022_interface::id(),
1361            &fee_payer_token_account,
1362            &mint,
1363            &delegate,
1364            &fee_payer,
1365            &[],
1366            1000,
1367            2,
1368        )
1369        .unwrap();
1370
1371        let message =
1372            VersionedMessage::Legacy(Message::new(&[approve_checked_ix], Some(&fee_payer)));
1373        let mut transaction =
1374            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1375
1376        // Should also fail for approve_checked
1377        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1378    }
1379
1380    #[tokio::test]
1381    #[serial]
1382    async fn test_fee_payer_policy_create_account() {
1383        use solana_system_interface::instruction::create_account;
1384
1385        let fee_payer = Pubkey::new_unique();
1386        let new_account = Pubkey::new_unique();
1387        let owner = Pubkey::new_unique();
1388
1389        // Test with allow_create_account = true
1390        let rpc_client = RpcMockBuilder::new().build();
1391        let mut policy = FeePayerPolicy::default();
1392        policy.system.allow_create_account = true;
1393        setup_config_with_policy(policy);
1394
1395        let validator = TransactionValidator::new(fee_payer).unwrap();
1396        let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner);
1397        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1398        let mut transaction =
1399            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1400        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1401
1402        // Test with allow_create_account = false
1403        let rpc_client = RpcMockBuilder::new().build();
1404        let mut policy = FeePayerPolicy::default();
1405        policy.system.allow_create_account = false;
1406        setup_config_with_policy(policy);
1407
1408        let validator = TransactionValidator::new(fee_payer).unwrap();
1409        let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner);
1410        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1411        let mut transaction =
1412            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1413        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1414    }
1415
1416    #[tokio::test]
1417    #[serial]
1418    async fn test_fee_payer_policy_allocate() {
1419        use solana_system_interface::instruction::allocate;
1420
1421        let fee_payer = Pubkey::new_unique();
1422
1423        // Test with allow_allocate = true
1424        let rpc_client = RpcMockBuilder::new().build();
1425        let mut policy = FeePayerPolicy::default();
1426        policy.system.allow_allocate = true;
1427        setup_config_with_policy(policy);
1428
1429        let validator = TransactionValidator::new(fee_payer).unwrap();
1430        let instruction = allocate(&fee_payer, 100);
1431        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1432        let mut transaction =
1433            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1434        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1435
1436        // Test with allow_allocate = false
1437        let rpc_client = RpcMockBuilder::new().build();
1438        let mut policy = FeePayerPolicy::default();
1439        policy.system.allow_allocate = false;
1440        setup_config_with_policy(policy);
1441
1442        let validator = TransactionValidator::new(fee_payer).unwrap();
1443        let instruction = allocate(&fee_payer, 100);
1444        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1445        let mut transaction =
1446            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1447        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1448    }
1449
1450    #[tokio::test]
1451    #[serial]
1452    async fn test_fee_payer_policy_nonce_initialize() {
1453        use solana_system_interface::instruction::create_nonce_account;
1454
1455        let fee_payer = Pubkey::new_unique();
1456        let nonce_account = Pubkey::new_unique();
1457
1458        // Test with allow_initialize = true
1459        let rpc_client = RpcMockBuilder::new().build();
1460        let mut policy = FeePayerPolicy::default();
1461        policy.system.nonce.allow_initialize = true;
1462        setup_config_with_policy(policy);
1463
1464        let validator = TransactionValidator::new(fee_payer).unwrap();
1465        let instructions = create_nonce_account(&fee_payer, &nonce_account, &fee_payer, 1_000_000);
1466        // Only test the InitializeNonceAccount instruction (second one)
1467        let message =
1468            VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer)));
1469        let mut transaction =
1470            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1471        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1472
1473        // Test with allow_initialize = false
1474        let rpc_client = RpcMockBuilder::new().build();
1475        let mut policy = FeePayerPolicy::default();
1476        policy.system.nonce.allow_initialize = false;
1477        setup_config_with_policy(policy);
1478
1479        let validator = TransactionValidator::new(fee_payer).unwrap();
1480        let instructions = create_nonce_account(&fee_payer, &nonce_account, &fee_payer, 1_000_000);
1481        let message =
1482            VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer)));
1483        let mut transaction =
1484            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1485        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1486    }
1487
1488    #[tokio::test]
1489    #[serial]
1490    async fn test_fee_payer_policy_nonce_advance() {
1491        use solana_system_interface::instruction::advance_nonce_account;
1492
1493        let fee_payer = Pubkey::new_unique();
1494        let nonce_account = Pubkey::new_unique();
1495
1496        // Test with allow_advance = true (must also enable durable transactions)
1497        let rpc_client = RpcMockBuilder::new().build();
1498        let mut policy = FeePayerPolicy::default();
1499        policy.system.nonce.allow_advance = true;
1500        let config = ConfigMockBuilder::new()
1501            .with_price_source(PriceSource::Mock)
1502            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
1503            .with_max_allowed_lamports(1_000_000)
1504            .with_fee_payer_policy(policy)
1505            .with_allow_durable_transactions(true)
1506            .build();
1507        update_config(config).unwrap();
1508
1509        let validator = TransactionValidator::new(fee_payer).unwrap();
1510        let instruction = advance_nonce_account(&nonce_account, &fee_payer);
1511        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1512        let mut transaction =
1513            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1514        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1515
1516        // Test with allow_advance = false (durable txs enabled but policy blocks it)
1517        let rpc_client = RpcMockBuilder::new().build();
1518        let mut policy = FeePayerPolicy::default();
1519        policy.system.nonce.allow_advance = false;
1520        let config = ConfigMockBuilder::new()
1521            .with_price_source(PriceSource::Mock)
1522            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
1523            .with_max_allowed_lamports(1_000_000)
1524            .with_fee_payer_policy(policy)
1525            .with_allow_durable_transactions(true)
1526            .build();
1527        update_config(config).unwrap();
1528
1529        let validator = TransactionValidator::new(fee_payer).unwrap();
1530        let instruction = advance_nonce_account(&nonce_account, &fee_payer);
1531        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1532        let mut transaction =
1533            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1534        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1535    }
1536
1537    #[tokio::test]
1538    #[serial]
1539    async fn test_fee_payer_policy_nonce_withdraw() {
1540        use solana_system_interface::instruction::withdraw_nonce_account;
1541
1542        let fee_payer = Pubkey::new_unique();
1543        let nonce_account = Pubkey::new_unique();
1544        let recipient = Pubkey::new_unique();
1545
1546        // Test with allow_withdraw = true
1547        let rpc_client = RpcMockBuilder::new().build();
1548        let mut policy = FeePayerPolicy::default();
1549        policy.system.nonce.allow_withdraw = true;
1550        setup_config_with_policy(policy);
1551
1552        let validator = TransactionValidator::new(fee_payer).unwrap();
1553        let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000);
1554        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1555        let mut transaction =
1556            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1557        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1558
1559        // Test with allow_withdraw = false
1560        let rpc_client = RpcMockBuilder::new().build();
1561        let mut policy = FeePayerPolicy::default();
1562        policy.system.nonce.allow_withdraw = false;
1563        setup_config_with_policy(policy);
1564
1565        let validator = TransactionValidator::new(fee_payer).unwrap();
1566        let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000);
1567        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1568        let mut transaction =
1569            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1570        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1571    }
1572
1573    #[tokio::test]
1574    #[serial]
1575    async fn test_fee_payer_policy_nonce_authorize() {
1576        use solana_system_interface::instruction::authorize_nonce_account;
1577
1578        let fee_payer = Pubkey::new_unique();
1579        let nonce_account = Pubkey::new_unique();
1580        let new_authority = Pubkey::new_unique();
1581
1582        // Test with allow_authorize = true
1583        let rpc_client = RpcMockBuilder::new().build();
1584        let mut policy = FeePayerPolicy::default();
1585        policy.system.nonce.allow_authorize = true;
1586        setup_config_with_policy(policy);
1587
1588        let validator = TransactionValidator::new(fee_payer).unwrap();
1589        let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority);
1590        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1591        let mut transaction =
1592            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1593        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1594
1595        // Test with allow_authorize = false
1596        let rpc_client = RpcMockBuilder::new().build();
1597        let mut policy = FeePayerPolicy::default();
1598        policy.system.nonce.allow_authorize = false;
1599        setup_config_with_policy(policy);
1600
1601        let validator = TransactionValidator::new(fee_payer).unwrap();
1602        let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority);
1603        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1604        let mut transaction =
1605            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1606        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1607    }
1608
1609    #[test]
1610    #[serial]
1611    fn test_strict_pricing_total_exceeds_fixed() {
1612        let mut config = ConfigMockBuilder::new().build();
1613        config.validation.price.model = PriceModel::Fixed {
1614            amount: 5000,
1615            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1616            strict: true,
1617        };
1618        let _ = update_config(config);
1619
1620        // Fixed price = 5000, but total = 3000 + 2000 + 5000 = 10000 > 5000
1621        let fee_calc = TotalFeeCalculation::new(5000, 3000, 2000, 5000, 0, 0);
1622
1623        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1624
1625        assert!(result.is_err());
1626        if let Err(KoraError::ValidationError(msg)) = result {
1627            assert!(msg.contains("Strict pricing violation"));
1628            assert!(msg.contains("exceeds fixed price"));
1629        } else {
1630            panic!("Expected ValidationError");
1631        }
1632    }
1633
1634    #[test]
1635    #[serial]
1636    fn test_strict_pricing_total_within_fixed() {
1637        let mut config = ConfigMockBuilder::new().build();
1638        config.validation.price.model = PriceModel::Fixed {
1639            amount: 5000,
1640            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1641            strict: true,
1642        };
1643        let _ = update_config(config);
1644
1645        // Fixed price = 5000, total = 1000 + 1000 + 1000 = 3000 < 5000
1646        let fee_calc = TotalFeeCalculation::new(5000, 1000, 1000, 1000, 0, 0);
1647
1648        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1649
1650        assert!(result.is_ok());
1651    }
1652
1653    #[test]
1654    #[serial]
1655    fn test_strict_pricing_disabled() {
1656        let mut config = ConfigMockBuilder::new().build();
1657        config.validation.price.model = PriceModel::Fixed {
1658            amount: 5000,
1659            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1660            strict: false, // Disabled
1661        };
1662        let _ = update_config(config);
1663
1664        let fee_calc = TotalFeeCalculation::new(5000, 10000, 0, 0, 0, 0);
1665
1666        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1667
1668        assert!(result.is_ok(), "Should pass when strict=false");
1669    }
1670
1671    #[test]
1672    #[serial]
1673    fn test_strict_pricing_with_margin_pricing() {
1674        use crate::{
1675            fee::price::PriceModel, state::update_config, tests::config_mock::ConfigMockBuilder,
1676        };
1677
1678        let mut config = ConfigMockBuilder::new().build();
1679        config.validation.price.model = PriceModel::Margin { margin: 0.1 };
1680        let _ = update_config(config);
1681
1682        let fee_calc = TotalFeeCalculation::new(5000, 10000, 0, 0, 0, 0);
1683
1684        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1685
1686        assert!(result.is_ok());
1687    }
1688
1689    #[test]
1690    #[serial]
1691    fn test_strict_pricing_exact_match() {
1692        use crate::{
1693            fee::price::PriceModel, state::update_config, tests::config_mock::ConfigMockBuilder,
1694        };
1695
1696        let mut config = ConfigMockBuilder::new().build();
1697        config.validation.price.model = PriceModel::Fixed {
1698            amount: 5000,
1699            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1700            strict: true,
1701        };
1702        let _ = update_config(config);
1703
1704        // Total exactly equals fixed price (5000 = 5000)
1705        let fee_calc = TotalFeeCalculation::new(5000, 2000, 1000, 2000, 0, 0);
1706
1707        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1708
1709        assert!(result.is_ok(), "Should pass when total equals fixed price");
1710    }
1711
1712    #[tokio::test]
1713    #[serial]
1714    async fn test_durable_transaction_rejected_by_default() {
1715        use solana_system_interface::instruction::advance_nonce_account;
1716
1717        let fee_payer = Pubkey::new_unique();
1718        let nonce_account = Pubkey::new_unique();
1719        let nonce_authority = Pubkey::new_unique(); // Different from fee payer
1720
1721        // Default config has allow_durable_transactions = false
1722        setup_default_config();
1723        let rpc_client = RpcMockBuilder::new().build();
1724
1725        let validator = TransactionValidator::new(fee_payer).unwrap();
1726
1727        // Transaction with AdvanceNonceAccount (authority is NOT fee payer)
1728        let instruction = advance_nonce_account(&nonce_account, &nonce_authority);
1729        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1730        let mut transaction =
1731            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1732
1733        let result = validator.validate_transaction(&mut transaction, &rpc_client).await;
1734        assert!(result.is_err());
1735        if let Err(KoraError::InvalidTransaction(msg)) = result {
1736            assert!(msg.contains("Durable transactions"));
1737            assert!(msg.contains("not allowed"));
1738        } else {
1739            panic!("Expected InvalidTransaction error");
1740        }
1741    }
1742
1743    #[tokio::test]
1744    #[serial]
1745    async fn test_durable_transaction_allowed_when_enabled() {
1746        use solana_system_interface::instruction::advance_nonce_account;
1747
1748        let fee_payer = Pubkey::new_unique();
1749        let nonce_account = Pubkey::new_unique();
1750        let nonce_authority = Pubkey::new_unique(); // Different from fee payer
1751
1752        // Enable durable transactions
1753        let config = ConfigMockBuilder::new()
1754            .with_price_source(PriceSource::Mock)
1755            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
1756            .with_max_allowed_lamports(1_000_000)
1757            .with_fee_payer_policy(FeePayerPolicy::default())
1758            .with_allow_durable_transactions(true)
1759            .build();
1760        update_config(config).unwrap();
1761
1762        let rpc_client = RpcMockBuilder::new().build();
1763        let validator = TransactionValidator::new(fee_payer).unwrap();
1764
1765        // Transaction with AdvanceNonceAccount (authority is NOT fee payer)
1766        let instruction = advance_nonce_account(&nonce_account, &nonce_authority);
1767        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1768        let mut transaction =
1769            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1770
1771        // Should pass because durable transactions are allowed
1772        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1773    }
1774
1775    #[tokio::test]
1776    #[serial]
1777    async fn test_non_durable_transaction_passes() {
1778        let fee_payer = Pubkey::new_unique();
1779        let sender = Pubkey::new_unique();
1780        let recipient = Pubkey::new_unique();
1781
1782        // Default config has allow_durable_transactions = false
1783        setup_default_config();
1784        let rpc_client = RpcMockBuilder::new().build();
1785
1786        let validator = TransactionValidator::new(fee_payer).unwrap();
1787
1788        // Regular transfer (no nonce instruction)
1789        let instruction = transfer(&sender, &recipient, 1000);
1790        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1791        let mut transaction =
1792            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1793
1794        // Should pass - no AdvanceNonceAccount instruction
1795        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1796    }
1797}