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::{Config, FeePayerPolicy},
409        state::update_config,
410        tests::{
411            account_mock::{MintAccountMockBuilder, TokenAccountMockBuilder},
412            config_mock::{mock_state::setup_config_mock, ConfigMockBuilder},
413            rpc_mock::RpcMockBuilder,
414        },
415        transaction::TransactionUtil,
416    };
417    use serial_test::serial;
418
419    use super::*;
420    use solana_message::{Message, VersionedMessage};
421    use solana_sdk::instruction::Instruction;
422    use solana_system_interface::{
423        instruction::{
424            assign, create_account, create_account_with_seed, transfer, transfer_with_seed,
425        },
426        program::ID as SYSTEM_PROGRAM_ID,
427    };
428
429    fn setup_both_configs(config: Config) {
430        drop(setup_config_mock(config.clone()));
431        update_config(config).unwrap();
432    }
433
434    // Helper functions to reduce test duplication and setup config
435    fn setup_default_config() {
436        let config = ConfigMockBuilder::new()
437            .with_price_source(PriceSource::Mock)
438            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
439            .with_max_allowed_lamports(1_000_000)
440            .with_fee_payer_policy(FeePayerPolicy::default())
441            .build();
442        setup_both_configs(config);
443    }
444
445    fn setup_config_with_policy(policy: FeePayerPolicy) {
446        let config = ConfigMockBuilder::new()
447            .with_price_source(PriceSource::Mock)
448            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
449            .with_max_allowed_lamports(1_000_000)
450            .with_fee_payer_policy(policy)
451            .build();
452        setup_both_configs(config);
453    }
454
455    fn setup_spl_config_with_policy(policy: FeePayerPolicy) {
456        let config = ConfigMockBuilder::new()
457            .with_price_source(PriceSource::Mock)
458            .with_allowed_programs(vec![spl_token_interface::id().to_string()])
459            .with_max_allowed_lamports(1_000_000)
460            .with_fee_payer_policy(policy)
461            .build();
462        setup_both_configs(config);
463    }
464
465    fn setup_token2022_config_with_policy(policy: FeePayerPolicy) {
466        let config = ConfigMockBuilder::new()
467            .with_price_source(PriceSource::Mock)
468            .with_allowed_programs(vec![spl_token_2022_interface::id().to_string()])
469            .with_max_allowed_lamports(1_000_000)
470            .with_fee_payer_policy(policy)
471            .build();
472        setup_both_configs(config);
473    }
474
475    #[tokio::test]
476    #[serial]
477    async fn test_validate_transaction() {
478        let fee_payer = Pubkey::new_unique();
479        setup_default_config();
480        let rpc_client = RpcMockBuilder::new().build();
481
482        let validator = TransactionValidator::new(fee_payer).unwrap();
483
484        let recipient = Pubkey::new_unique();
485        let sender = Pubkey::new_unique();
486        let instruction = transfer(&sender, &recipient, 100_000);
487        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
488        let mut transaction =
489            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
490
491        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
492    }
493
494    #[tokio::test]
495    #[serial]
496    async fn test_transfer_amount_limits() {
497        let fee_payer = Pubkey::new_unique();
498        setup_default_config();
499        let rpc_client = RpcMockBuilder::new().build();
500
501        let validator = TransactionValidator::new(fee_payer).unwrap();
502        let sender = Pubkey::new_unique();
503        let recipient = Pubkey::new_unique();
504
505        // Test transaction with amount over limit
506        let instruction = transfer(&sender, &recipient, 2_000_000);
507        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
508        let mut transaction =
509            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
510
511        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
512
513        // Test multiple transfers
514        let instructions =
515            vec![transfer(&sender, &recipient, 500_000), transfer(&sender, &recipient, 500_000)];
516        let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
517        let mut transaction =
518            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
519        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
520    }
521
522    #[tokio::test]
523    #[serial]
524    async fn test_validate_programs() {
525        let fee_payer = Pubkey::new_unique();
526        setup_default_config();
527        let rpc_client = RpcMockBuilder::new().build();
528
529        let validator = TransactionValidator::new(fee_payer).unwrap();
530        let sender = Pubkey::new_unique();
531        let recipient = Pubkey::new_unique();
532
533        // Test allowed program (system program)
534        let instruction = transfer(&sender, &recipient, 1000);
535        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
536        let mut transaction =
537            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
538        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
539
540        // Test disallowed program
541        let fake_program = Pubkey::new_unique();
542        // Create a no-op instruction for the fake program
543        let instruction = Instruction::new_with_bincode(
544            fake_program,
545            &[0u8],
546            vec![], // no accounts needed for this test
547        );
548        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
549        let mut transaction =
550            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
551        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
552    }
553
554    #[tokio::test]
555    #[serial]
556    async fn test_validate_signatures() {
557        let fee_payer = Pubkey::new_unique();
558        let config = ConfigMockBuilder::new()
559            .with_price_source(PriceSource::Mock)
560            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
561            .with_max_allowed_lamports(1_000_000)
562            .with_max_signatures(2)
563            .with_fee_payer_policy(FeePayerPolicy::default())
564            .build();
565        update_config(config).unwrap();
566
567        let rpc_client = RpcMockBuilder::new().build();
568        let validator = TransactionValidator::new(fee_payer).unwrap();
569        let sender = Pubkey::new_unique();
570        let recipient = Pubkey::new_unique();
571
572        // Test too many signatures
573        let instructions = vec![
574            transfer(&sender, &recipient, 1000),
575            transfer(&sender, &recipient, 1000),
576            transfer(&sender, &recipient, 1000),
577        ];
578        let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
579        let mut transaction =
580            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
581        transaction.transaction.signatures = vec![Default::default(); 3]; // Add 3 dummy signatures
582        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
583    }
584
585    #[tokio::test]
586    #[serial]
587    async fn test_sign_and_send_transaction_mode() {
588        let fee_payer = Pubkey::new_unique();
589        setup_default_config();
590        let rpc_client = RpcMockBuilder::new().build();
591
592        let validator = TransactionValidator::new(fee_payer).unwrap();
593        let sender = Pubkey::new_unique();
594        let recipient = Pubkey::new_unique();
595
596        // Test SignAndSend mode with fee payer already set should not error
597        let instruction = transfer(&sender, &recipient, 1000);
598        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
599        let mut transaction =
600            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
601        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
602
603        // Test SignAndSend mode without fee payer (should succeed)
604        let instruction = transfer(&sender, &recipient, 1000);
605        let message = VersionedMessage::Legacy(Message::new(&[instruction], None)); // No fee payer specified
606        let mut transaction =
607            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
608        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
609    }
610
611    #[tokio::test]
612    #[serial]
613    async fn test_empty_transaction() {
614        let fee_payer = Pubkey::new_unique();
615        setup_default_config();
616        let rpc_client = RpcMockBuilder::new().build();
617
618        let validator = TransactionValidator::new(fee_payer).unwrap();
619
620        // Create an empty message using Message::new with empty instructions
621        let message = VersionedMessage::Legacy(Message::new(&[], Some(&fee_payer)));
622        let mut transaction =
623            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
624        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
625    }
626
627    #[tokio::test]
628    #[serial]
629    async fn test_disallowed_accounts() {
630        let fee_payer = Pubkey::new_unique();
631        let config = ConfigMockBuilder::new()
632            .with_price_source(PriceSource::Mock)
633            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
634            .with_max_allowed_lamports(1_000_000)
635            .with_disallowed_accounts(vec![
636                "hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek".to_string()
637            ])
638            .with_fee_payer_policy(FeePayerPolicy::default())
639            .build();
640        update_config(config).unwrap();
641
642        let rpc_client = RpcMockBuilder::new().build();
643        let validator = TransactionValidator::new(fee_payer).unwrap();
644        let instruction = transfer(
645            &Pubkey::from_str("hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek").unwrap(),
646            &fee_payer,
647            1000,
648        );
649        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
650        let mut transaction =
651            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
652        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
653    }
654
655    #[tokio::test]
656    #[serial]
657    async fn test_fee_payer_policy_sol_transfers() {
658        let fee_payer = Pubkey::new_unique();
659        let recipient = Pubkey::new_unique();
660
661        // Test with allow_sol_transfers = true
662        let rpc_client = RpcMockBuilder::new().build();
663        let mut policy = FeePayerPolicy::default();
664        policy.system.allow_transfer = true;
665        setup_config_with_policy(policy);
666
667        let validator = TransactionValidator::new(fee_payer).unwrap();
668
669        let instruction = transfer(&fee_payer, &recipient, 1000);
670
671        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
672        let mut transaction =
673            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
674        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
675
676        // Test with allow_sol_transfers = false
677        let rpc_client = RpcMockBuilder::new().build();
678        let mut policy = FeePayerPolicy::default();
679        policy.system.allow_transfer = false;
680        setup_config_with_policy(policy);
681
682        let validator = TransactionValidator::new(fee_payer).unwrap();
683
684        let instruction = transfer(&fee_payer, &recipient, 1000);
685        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
686        let mut transaction =
687            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
688        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
689    }
690
691    #[tokio::test]
692    #[serial]
693    async fn test_fee_payer_policy_assign() {
694        let fee_payer = Pubkey::new_unique();
695        let new_owner = Pubkey::new_unique();
696
697        // Test with allow_assign = true
698
699        let rpc_client = RpcMockBuilder::new().build();
700
701        let mut policy = FeePayerPolicy::default();
702        policy.system.allow_assign = true;
703        setup_config_with_policy(policy);
704
705        let validator = TransactionValidator::new(fee_payer).unwrap();
706
707        let instruction = assign(&fee_payer, &new_owner);
708        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
709        let mut transaction =
710            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
711        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
712
713        // Test with allow_assign = false
714
715        let rpc_client = RpcMockBuilder::new().build();
716
717        let mut policy = FeePayerPolicy::default();
718        policy.system.allow_assign = false;
719        setup_config_with_policy(policy);
720
721        let validator = TransactionValidator::new(fee_payer).unwrap();
722
723        let instruction = assign(&fee_payer, &new_owner);
724        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
725        let mut transaction =
726            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
727        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
728    }
729
730    #[tokio::test]
731    #[serial]
732    async fn test_fee_payer_policy_spl_transfers() {
733        let fee_payer = Pubkey::new_unique();
734
735        let fee_payer_token_account = Pubkey::new_unique();
736        let recipient_token_account = Pubkey::new_unique();
737        let mint = Pubkey::new_unique();
738
739        let source_token_account =
740            TokenAccountMockBuilder::new().with_mint(&mint).with_owner(&fee_payer).build();
741        let mint_account = MintAccountMockBuilder::new().with_decimals(6).build();
742
743        // Test with allow_spl_transfers = true (plain Transfer, mint resolved from source account)
744        let rpc_client = RpcMockBuilder::new()
745            .build_with_sequential_accounts(vec![&source_token_account, &mint_account]);
746
747        let mut policy = FeePayerPolicy::default();
748        policy.spl_token.allow_transfer = true;
749        setup_spl_config_with_policy(policy);
750
751        let validator = TransactionValidator::new(fee_payer).unwrap();
752
753        let transfer_ix = spl_token_interface::instruction::transfer(
754            &spl_token_interface::id(),
755            &fee_payer_token_account,
756            &recipient_token_account,
757            &fee_payer, // fee payer is the signer
758            &[],
759            1000,
760        )
761        .unwrap();
762
763        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
764        let mut transaction =
765            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
766        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
767
768        // Test with allow_spl_transfers = false
769        let rpc_client = RpcMockBuilder::new()
770            .build_with_sequential_accounts(vec![&source_token_account, &mint_account]);
771
772        let mut policy = FeePayerPolicy::default();
773        policy.spl_token.allow_transfer = false;
774        setup_spl_config_with_policy(policy);
775
776        let validator = TransactionValidator::new(fee_payer).unwrap();
777
778        let transfer_ix = spl_token_interface::instruction::transfer(
779            &spl_token_interface::id(),
780            &fee_payer_token_account,
781            &recipient_token_account,
782            &fee_payer, // fee payer is the signer
783            &[],
784            1000,
785        )
786        .unwrap();
787
788        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
789        let mut transaction =
790            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
791        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
792
793        // Test with other account as source - should always pass
794        let rpc_client = RpcMockBuilder::new().build();
795        let other_signer = Pubkey::new_unique();
796        let transfer_ix = spl_token_interface::instruction::transfer(
797            &spl_token_interface::id(),
798            &fee_payer_token_account,
799            &recipient_token_account,
800            &other_signer, // other account is the signer
801            &[],
802            1000,
803        )
804        .unwrap();
805
806        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
807        let mut transaction =
808            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
809        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
810    }
811
812    #[tokio::test]
813    #[serial]
814    async fn test_fee_payer_policy_token2022_transfers() {
815        let fee_payer = Pubkey::new_unique();
816
817        let fee_payer_token_account = Pubkey::new_unique();
818        let recipient_token_account = Pubkey::new_unique();
819        let mint = Pubkey::new_unique();
820
821        // Test with allow_token2022_transfers = true
822        let rpc_client = RpcMockBuilder::new()
823            .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation
824            .build();
825        // Test with token_2022.allow_transfer = true
826        let mut policy = FeePayerPolicy::default();
827        policy.token_2022.allow_transfer = true;
828        setup_token2022_config_with_policy(policy);
829
830        let validator = TransactionValidator::new(fee_payer).unwrap();
831
832        let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
833            &spl_token_2022_interface::id(),
834            &fee_payer_token_account,
835            &mint,
836            &recipient_token_account,
837            &fee_payer, // fee payer is the signer
838            &[],
839            1,
840            2,
841        )
842        .unwrap();
843
844        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
845        let mut transaction =
846            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
847        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
848
849        // Test with allow_token2022_transfers = false
850        let rpc_client = RpcMockBuilder::new()
851            .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation
852            .build();
853        let mut policy = FeePayerPolicy::default();
854        policy.token_2022.allow_transfer = false;
855        setup_token2022_config_with_policy(policy);
856
857        let validator = TransactionValidator::new(fee_payer).unwrap();
858
859        let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
860            &spl_token_2022_interface::id(),
861            &fee_payer_token_account,
862            &mint,
863            &recipient_token_account,
864            &fee_payer, // fee payer is the signer
865            &[],
866            1000,
867            2,
868        )
869        .unwrap();
870
871        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
872        let mut transaction =
873            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
874
875        // Should fail because fee payer is not allowed to be source
876        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
877
878        // Test with other account as source - should always pass
879        let other_signer = Pubkey::new_unique();
880        let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
881            &spl_token_2022_interface::id(),
882            &fee_payer_token_account,
883            &mint,
884            &recipient_token_account,
885            &other_signer, // other account is the signer
886            &[],
887            1000,
888            2,
889        )
890        .unwrap();
891
892        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
893        let mut transaction =
894            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
895
896        // Should pass because fee payer is not the source
897        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
898    }
899
900    #[tokio::test]
901    #[serial]
902    async fn test_calculate_total_outflow() {
903        let fee_payer = Pubkey::new_unique();
904        let config = ConfigMockBuilder::new()
905            .with_price_source(PriceSource::Mock)
906            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
907            .with_max_allowed_lamports(10_000_000)
908            .with_fee_payer_policy(FeePayerPolicy::default())
909            .build();
910        update_config(config).unwrap();
911
912        let rpc_client = RpcMockBuilder::new().build();
913        let validator = TransactionValidator::new(fee_payer).unwrap();
914
915        // Test 1: Fee payer as sender in Transfer - should add to outflow
916        let recipient = Pubkey::new_unique();
917        let transfer_instruction = transfer(&fee_payer, &recipient, 100_000);
918        let message =
919            VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
920        let mut transaction =
921            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
922        let outflow =
923            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
924        assert_eq!(outflow, 100_000, "Transfer from fee payer should add to outflow");
925
926        // Test 2: Fee payer as recipient in Transfer - should subtract from outflow (account closure)
927        let sender = Pubkey::new_unique();
928        let transfer_instruction = transfer(&sender, &fee_payer, 50_000);
929        let message =
930            VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
931        let mut transaction =
932            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
933
934        let outflow =
935            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
936        assert_eq!(outflow, 0, "Transfer to fee payer should subtract from outflow"); // 0 - 50_000 = 0 (saturating_sub)
937
938        // Test 3: Fee payer as funding account in CreateAccount - should add to outflow
939        let new_account = Pubkey::new_unique();
940        let create_instruction = create_account(
941            &fee_payer,
942            &new_account,
943            200_000, // lamports
944            100,     // space
945            &SYSTEM_PROGRAM_ID,
946        );
947        let message =
948            VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer)));
949        let mut transaction =
950            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
951        let outflow =
952            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
953        assert_eq!(outflow, 200_000, "CreateAccount funded by fee payer should add to outflow");
954
955        // Test 4: Fee payer as funding account in CreateAccountWithSeed - should add to outflow
956        let create_with_seed_instruction = create_account_with_seed(
957            &fee_payer,
958            &new_account,
959            &fee_payer,
960            "test_seed",
961            300_000, // lamports
962            100,     // space
963            &SYSTEM_PROGRAM_ID,
964        );
965        let message = VersionedMessage::Legacy(Message::new(
966            &[create_with_seed_instruction],
967            Some(&fee_payer),
968        ));
969        let mut transaction =
970            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
971        let outflow =
972            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
973        assert_eq!(
974            outflow, 300_000,
975            "CreateAccountWithSeed funded by fee payer should add to outflow"
976        );
977
978        // Test 5: TransferWithSeed from fee payer - should add to outflow
979        let transfer_with_seed_instruction = transfer_with_seed(
980            &fee_payer,
981            &fee_payer,
982            "test_seed".to_string(),
983            &SYSTEM_PROGRAM_ID,
984            &recipient,
985            150_000,
986        );
987        let message = VersionedMessage::Legacy(Message::new(
988            &[transfer_with_seed_instruction],
989            Some(&fee_payer),
990        ));
991        let mut transaction =
992            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
993        let outflow =
994            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
995        assert_eq!(outflow, 150_000, "TransferWithSeed from fee payer should add to outflow");
996
997        // Test 6: Multiple instructions - should sum correctly
998        let instructions = vec![
999            transfer(&fee_payer, &recipient, 100_000), // +100_000
1000            transfer(&sender, &fee_payer, 30_000),     // -30_000
1001            create_account(&fee_payer, &new_account, 50_000, 100, &SYSTEM_PROGRAM_ID), // +50_000
1002        ];
1003        let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
1004        let mut transaction =
1005            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1006        let outflow =
1007            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
1008        assert_eq!(
1009            outflow, 120_000,
1010            "Multiple instructions should sum correctly: 100000 - 30000 + 50000 = 120000"
1011        );
1012
1013        // Test 7: Other account as sender - should not affect outflow
1014        let other_sender = Pubkey::new_unique();
1015        let transfer_instruction = transfer(&other_sender, &recipient, 500_000);
1016        let message =
1017            VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
1018        let mut transaction =
1019            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1020        let outflow =
1021            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
1022        assert_eq!(outflow, 0, "Transfer from other account should not affect outflow");
1023
1024        // Test 8: Other account funding CreateAccount - should not affect outflow
1025        let other_funder = Pubkey::new_unique();
1026        let create_instruction =
1027            create_account(&other_funder, &new_account, 1_000_000, 100, &SYSTEM_PROGRAM_ID);
1028        let message =
1029            VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer)));
1030        let mut transaction =
1031            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1032        let outflow =
1033            validator.calculate_total_outflow(&mut transaction, &rpc_client).await.unwrap();
1034        assert_eq!(outflow, 0, "CreateAccount funded by other account should not affect outflow");
1035    }
1036
1037    #[tokio::test]
1038    #[serial]
1039    async fn test_fee_payer_policy_burn() {
1040        let fee_payer = Pubkey::new_unique();
1041        let fee_payer_token_account = Pubkey::new_unique();
1042        let mint = Pubkey::new_unique();
1043
1044        // Test with allow_burn = true
1045
1046        let rpc_client = RpcMockBuilder::new().build();
1047        let mut policy = FeePayerPolicy::default();
1048        policy.spl_token.allow_burn = true;
1049        setup_spl_config_with_policy(policy);
1050
1051        let validator = TransactionValidator::new(fee_payer).unwrap();
1052
1053        let burn_ix = spl_token_interface::instruction::burn(
1054            &spl_token_interface::id(),
1055            &fee_payer_token_account,
1056            &mint,
1057            &fee_payer,
1058            &[],
1059            1000,
1060        )
1061        .unwrap();
1062
1063        let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer)));
1064        let mut transaction =
1065            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1066        // Should pass because allow_burn is true by default
1067        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1068
1069        // Test with allow_burn = false
1070
1071        let rpc_client = RpcMockBuilder::new().build();
1072        let mut policy = FeePayerPolicy::default();
1073        policy.spl_token.allow_burn = false;
1074        setup_spl_config_with_policy(policy);
1075
1076        let validator = TransactionValidator::new(fee_payer).unwrap();
1077
1078        let burn_ix = spl_token_interface::instruction::burn(
1079            &spl_token_interface::id(),
1080            &fee_payer_token_account,
1081            &mint,
1082            &fee_payer,
1083            &[],
1084            1000,
1085        )
1086        .unwrap();
1087
1088        let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer)));
1089        let mut transaction =
1090            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1091
1092        // Should fail because fee payer cannot burn tokens when allow_burn is false
1093        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1094
1095        // Test burn_checked instruction
1096        let burn_checked_ix = spl_token_interface::instruction::burn_checked(
1097            &spl_token_interface::id(),
1098            &fee_payer_token_account,
1099            &mint,
1100            &fee_payer,
1101            &[],
1102            1000,
1103            2,
1104        )
1105        .unwrap();
1106
1107        let message = VersionedMessage::Legacy(Message::new(&[burn_checked_ix], Some(&fee_payer)));
1108        let mut transaction =
1109            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1110
1111        // Should also fail for burn_checked
1112        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1113    }
1114
1115    #[tokio::test]
1116    #[serial]
1117    async fn test_fee_payer_policy_close_account() {
1118        let fee_payer = Pubkey::new_unique();
1119        let fee_payer_token_account = Pubkey::new_unique();
1120        let destination = Pubkey::new_unique();
1121
1122        // Test with allow_close_account = true
1123
1124        let rpc_client = RpcMockBuilder::new().build();
1125        let mut policy = FeePayerPolicy::default();
1126        policy.spl_token.allow_close_account = true;
1127        setup_spl_config_with_policy(policy);
1128
1129        let validator = TransactionValidator::new(fee_payer).unwrap();
1130
1131        let close_ix = spl_token_interface::instruction::close_account(
1132            &spl_token_interface::id(),
1133            &fee_payer_token_account,
1134            &destination,
1135            &fee_payer,
1136            &[],
1137        )
1138        .unwrap();
1139
1140        let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer)));
1141        let mut transaction =
1142            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1143        // Should pass because allow_close_account is true by default
1144        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1145
1146        // Test with allow_close_account = false
1147        let rpc_client = RpcMockBuilder::new().build();
1148        let mut policy = FeePayerPolicy::default();
1149        policy.spl_token.allow_close_account = false;
1150        setup_spl_config_with_policy(policy);
1151
1152        let validator = TransactionValidator::new(fee_payer).unwrap();
1153
1154        let close_ix = spl_token_interface::instruction::close_account(
1155            &spl_token_interface::id(),
1156            &fee_payer_token_account,
1157            &destination,
1158            &fee_payer,
1159            &[],
1160        )
1161        .unwrap();
1162
1163        let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer)));
1164        let mut transaction =
1165            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1166
1167        // Should fail because fee payer cannot close accounts when allow_close_account is false
1168        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1169    }
1170
1171    #[tokio::test]
1172    #[serial]
1173    async fn test_fee_payer_policy_approve() {
1174        let fee_payer = Pubkey::new_unique();
1175        let fee_payer_token_account = Pubkey::new_unique();
1176        let delegate = Pubkey::new_unique();
1177
1178        // Test with allow_approve = true
1179
1180        let rpc_client = RpcMockBuilder::new().build();
1181        let mut policy = FeePayerPolicy::default();
1182        policy.spl_token.allow_approve = true;
1183        setup_spl_config_with_policy(policy);
1184
1185        let validator = TransactionValidator::new(fee_payer).unwrap();
1186
1187        let approve_ix = spl_token_interface::instruction::approve(
1188            &spl_token_interface::id(),
1189            &fee_payer_token_account,
1190            &delegate,
1191            &fee_payer,
1192            &[],
1193            1000,
1194        )
1195        .unwrap();
1196
1197        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1198        let mut transaction =
1199            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1200        // Should pass because allow_approve is true by default
1201        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1202
1203        // Test with allow_approve = false
1204        let rpc_client = RpcMockBuilder::new().build();
1205        let mut policy = FeePayerPolicy::default();
1206        policy.spl_token.allow_approve = false;
1207        setup_spl_config_with_policy(policy);
1208
1209        let validator = TransactionValidator::new(fee_payer).unwrap();
1210
1211        let approve_ix = spl_token_interface::instruction::approve(
1212            &spl_token_interface::id(),
1213            &fee_payer_token_account,
1214            &delegate,
1215            &fee_payer,
1216            &[],
1217            1000,
1218        )
1219        .unwrap();
1220
1221        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1222        let mut transaction =
1223            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1224
1225        // Should fail because fee payer cannot approve when allow_approve is false
1226        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1227
1228        // Test approve_checked instruction
1229        let mint = Pubkey::new_unique();
1230        let approve_checked_ix = spl_token_interface::instruction::approve_checked(
1231            &spl_token_interface::id(),
1232            &fee_payer_token_account,
1233            &mint,
1234            &delegate,
1235            &fee_payer,
1236            &[],
1237            1000,
1238            2,
1239        )
1240        .unwrap();
1241
1242        let message =
1243            VersionedMessage::Legacy(Message::new(&[approve_checked_ix], Some(&fee_payer)));
1244        let mut transaction =
1245            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1246
1247        // Should also fail for approve_checked
1248        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1249    }
1250
1251    #[tokio::test]
1252    #[serial]
1253    async fn test_fee_payer_policy_token2022_burn() {
1254        let fee_payer = Pubkey::new_unique();
1255        let fee_payer_token_account = Pubkey::new_unique();
1256        let mint = Pubkey::new_unique();
1257
1258        // Test with allow_burn = false for Token2022
1259
1260        let rpc_client = RpcMockBuilder::new().build();
1261        let mut policy = FeePayerPolicy::default();
1262        policy.token_2022.allow_burn = false;
1263        setup_token2022_config_with_policy(policy);
1264
1265        let validator = TransactionValidator::new(fee_payer).unwrap();
1266
1267        let burn_ix = spl_token_2022_interface::instruction::burn(
1268            &spl_token_2022_interface::id(),
1269            &fee_payer_token_account,
1270            &mint,
1271            &fee_payer,
1272            &[],
1273            1000,
1274        )
1275        .unwrap();
1276
1277        let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer)));
1278        let mut transaction =
1279            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1280        // Should fail for Token2022 burn
1281        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1282    }
1283
1284    #[tokio::test]
1285    #[serial]
1286    async fn test_fee_payer_policy_token2022_close_account() {
1287        let fee_payer = Pubkey::new_unique();
1288        let fee_payer_token_account = Pubkey::new_unique();
1289        let destination = Pubkey::new_unique();
1290
1291        // Test with allow_close_account = false for Token2022
1292
1293        let rpc_client = RpcMockBuilder::new().build();
1294        let mut policy = FeePayerPolicy::default();
1295        policy.token_2022.allow_close_account = false;
1296        setup_token2022_config_with_policy(policy);
1297
1298        let validator = TransactionValidator::new(fee_payer).unwrap();
1299
1300        let close_ix = spl_token_2022_interface::instruction::close_account(
1301            &spl_token_2022_interface::id(),
1302            &fee_payer_token_account,
1303            &destination,
1304            &fee_payer,
1305            &[],
1306        )
1307        .unwrap();
1308
1309        let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer)));
1310        let mut transaction =
1311            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1312        // Should fail for Token2022 close account
1313        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1314    }
1315
1316    #[tokio::test]
1317    #[serial]
1318    async fn test_fee_payer_policy_token2022_approve() {
1319        let fee_payer = Pubkey::new_unique();
1320        let fee_payer_token_account = Pubkey::new_unique();
1321        let delegate = Pubkey::new_unique();
1322
1323        // Test with allow_approve = true
1324
1325        let rpc_client = RpcMockBuilder::new().build();
1326        let mut policy = FeePayerPolicy::default();
1327        policy.token_2022.allow_approve = true;
1328        setup_token2022_config_with_policy(policy);
1329
1330        let validator = TransactionValidator::new(fee_payer).unwrap();
1331
1332        let approve_ix = spl_token_2022_interface::instruction::approve(
1333            &spl_token_2022_interface::id(),
1334            &fee_payer_token_account,
1335            &delegate,
1336            &fee_payer,
1337            &[],
1338            1000,
1339        )
1340        .unwrap();
1341
1342        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1343        let mut transaction =
1344            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1345        // Should pass because allow_approve is true by default
1346        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1347
1348        // Test with allow_approve = false
1349
1350        let rpc_client = RpcMockBuilder::new().build();
1351        let mut policy = FeePayerPolicy::default();
1352        policy.token_2022.allow_approve = false;
1353        setup_token2022_config_with_policy(policy);
1354
1355        let validator = TransactionValidator::new(fee_payer).unwrap();
1356
1357        let approve_ix = spl_token_2022_interface::instruction::approve(
1358            &spl_token_2022_interface::id(),
1359            &fee_payer_token_account,
1360            &delegate,
1361            &fee_payer,
1362            &[],
1363            1000,
1364        )
1365        .unwrap();
1366
1367        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1368        let mut transaction =
1369            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1370
1371        // Should fail because fee payer cannot approve when allow_approve is false
1372        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1373
1374        // Test approve_checked instruction
1375        let mint = Pubkey::new_unique();
1376        let approve_checked_ix = spl_token_2022_interface::instruction::approve_checked(
1377            &spl_token_2022_interface::id(),
1378            &fee_payer_token_account,
1379            &mint,
1380            &delegate,
1381            &fee_payer,
1382            &[],
1383            1000,
1384            2,
1385        )
1386        .unwrap();
1387
1388        let message =
1389            VersionedMessage::Legacy(Message::new(&[approve_checked_ix], Some(&fee_payer)));
1390        let mut transaction =
1391            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1392
1393        // Should also fail for approve_checked
1394        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1395    }
1396
1397    #[tokio::test]
1398    #[serial]
1399    async fn test_fee_payer_policy_create_account() {
1400        use solana_system_interface::instruction::create_account;
1401
1402        let fee_payer = Pubkey::new_unique();
1403        let new_account = Pubkey::new_unique();
1404        let owner = Pubkey::new_unique();
1405
1406        // Test with allow_create_account = true
1407        let rpc_client = RpcMockBuilder::new().build();
1408        let mut policy = FeePayerPolicy::default();
1409        policy.system.allow_create_account = true;
1410        setup_config_with_policy(policy);
1411
1412        let validator = TransactionValidator::new(fee_payer).unwrap();
1413        let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner);
1414        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1415        let mut transaction =
1416            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1417        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1418
1419        // Test with allow_create_account = false
1420        let rpc_client = RpcMockBuilder::new().build();
1421        let mut policy = FeePayerPolicy::default();
1422        policy.system.allow_create_account = false;
1423        setup_config_with_policy(policy);
1424
1425        let validator = TransactionValidator::new(fee_payer).unwrap();
1426        let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner);
1427        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1428        let mut transaction =
1429            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1430        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1431    }
1432
1433    #[tokio::test]
1434    #[serial]
1435    async fn test_fee_payer_policy_allocate() {
1436        use solana_system_interface::instruction::allocate;
1437
1438        let fee_payer = Pubkey::new_unique();
1439
1440        // Test with allow_allocate = true
1441        let rpc_client = RpcMockBuilder::new().build();
1442        let mut policy = FeePayerPolicy::default();
1443        policy.system.allow_allocate = true;
1444        setup_config_with_policy(policy);
1445
1446        let validator = TransactionValidator::new(fee_payer).unwrap();
1447        let instruction = allocate(&fee_payer, 100);
1448        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1449        let mut transaction =
1450            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1451        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1452
1453        // Test with allow_allocate = false
1454        let rpc_client = RpcMockBuilder::new().build();
1455        let mut policy = FeePayerPolicy::default();
1456        policy.system.allow_allocate = false;
1457        setup_config_with_policy(policy);
1458
1459        let validator = TransactionValidator::new(fee_payer).unwrap();
1460        let instruction = allocate(&fee_payer, 100);
1461        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1462        let mut transaction =
1463            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1464        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1465    }
1466
1467    #[tokio::test]
1468    #[serial]
1469    async fn test_fee_payer_policy_nonce_initialize() {
1470        use solana_system_interface::instruction::create_nonce_account;
1471
1472        let fee_payer = Pubkey::new_unique();
1473        let nonce_account = Pubkey::new_unique();
1474
1475        // Test with allow_initialize = true
1476        let rpc_client = RpcMockBuilder::new().build();
1477        let mut policy = FeePayerPolicy::default();
1478        policy.system.nonce.allow_initialize = true;
1479        setup_config_with_policy(policy);
1480
1481        let validator = TransactionValidator::new(fee_payer).unwrap();
1482        let instructions = create_nonce_account(&fee_payer, &nonce_account, &fee_payer, 1_000_000);
1483        // Only test the InitializeNonceAccount instruction (second one)
1484        let message =
1485            VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer)));
1486        let mut transaction =
1487            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1488        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1489
1490        // Test with allow_initialize = false
1491        let rpc_client = RpcMockBuilder::new().build();
1492        let mut policy = FeePayerPolicy::default();
1493        policy.system.nonce.allow_initialize = false;
1494        setup_config_with_policy(policy);
1495
1496        let validator = TransactionValidator::new(fee_payer).unwrap();
1497        let instructions = create_nonce_account(&fee_payer, &nonce_account, &fee_payer, 1_000_000);
1498        let message =
1499            VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer)));
1500        let mut transaction =
1501            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1502        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1503    }
1504
1505    #[tokio::test]
1506    #[serial]
1507    async fn test_fee_payer_policy_nonce_advance() {
1508        use solana_system_interface::instruction::advance_nonce_account;
1509
1510        let fee_payer = Pubkey::new_unique();
1511        let nonce_account = Pubkey::new_unique();
1512
1513        // Test with allow_advance = true (must also enable durable transactions)
1514        let rpc_client = RpcMockBuilder::new().build();
1515        let mut policy = FeePayerPolicy::default();
1516        policy.system.nonce.allow_advance = true;
1517        let config = ConfigMockBuilder::new()
1518            .with_price_source(PriceSource::Mock)
1519            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
1520            .with_max_allowed_lamports(1_000_000)
1521            .with_fee_payer_policy(policy)
1522            .with_allow_durable_transactions(true)
1523            .build();
1524        update_config(config).unwrap();
1525
1526        let validator = TransactionValidator::new(fee_payer).unwrap();
1527        let instruction = advance_nonce_account(&nonce_account, &fee_payer);
1528        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1529        let mut transaction =
1530            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1531        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1532
1533        // Test with allow_advance = false (durable txs enabled but policy blocks it)
1534        let rpc_client = RpcMockBuilder::new().build();
1535        let mut policy = FeePayerPolicy::default();
1536        policy.system.nonce.allow_advance = false;
1537        let config = ConfigMockBuilder::new()
1538            .with_price_source(PriceSource::Mock)
1539            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
1540            .with_max_allowed_lamports(1_000_000)
1541            .with_fee_payer_policy(policy)
1542            .with_allow_durable_transactions(true)
1543            .build();
1544        update_config(config).unwrap();
1545
1546        let validator = TransactionValidator::new(fee_payer).unwrap();
1547        let instruction = advance_nonce_account(&nonce_account, &fee_payer);
1548        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1549        let mut transaction =
1550            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1551        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1552    }
1553
1554    #[tokio::test]
1555    #[serial]
1556    async fn test_fee_payer_policy_nonce_withdraw() {
1557        use solana_system_interface::instruction::withdraw_nonce_account;
1558
1559        let fee_payer = Pubkey::new_unique();
1560        let nonce_account = Pubkey::new_unique();
1561        let recipient = Pubkey::new_unique();
1562
1563        // Test with allow_withdraw = true
1564        let rpc_client = RpcMockBuilder::new().build();
1565        let mut policy = FeePayerPolicy::default();
1566        policy.system.nonce.allow_withdraw = true;
1567        setup_config_with_policy(policy);
1568
1569        let validator = TransactionValidator::new(fee_payer).unwrap();
1570        let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000);
1571        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1572        let mut transaction =
1573            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1574        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1575
1576        // Test with allow_withdraw = false
1577        let rpc_client = RpcMockBuilder::new().build();
1578        let mut policy = FeePayerPolicy::default();
1579        policy.system.nonce.allow_withdraw = false;
1580        setup_config_with_policy(policy);
1581
1582        let validator = TransactionValidator::new(fee_payer).unwrap();
1583        let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000);
1584        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1585        let mut transaction =
1586            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1587        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1588    }
1589
1590    #[tokio::test]
1591    #[serial]
1592    async fn test_fee_payer_policy_nonce_authorize() {
1593        use solana_system_interface::instruction::authorize_nonce_account;
1594
1595        let fee_payer = Pubkey::new_unique();
1596        let nonce_account = Pubkey::new_unique();
1597        let new_authority = Pubkey::new_unique();
1598
1599        // Test with allow_authorize = true
1600        let rpc_client = RpcMockBuilder::new().build();
1601        let mut policy = FeePayerPolicy::default();
1602        policy.system.nonce.allow_authorize = true;
1603        setup_config_with_policy(policy);
1604
1605        let validator = TransactionValidator::new(fee_payer).unwrap();
1606        let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority);
1607        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1608        let mut transaction =
1609            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1610        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1611
1612        // Test with allow_authorize = false
1613        let rpc_client = RpcMockBuilder::new().build();
1614        let mut policy = FeePayerPolicy::default();
1615        policy.system.nonce.allow_authorize = false;
1616        setup_config_with_policy(policy);
1617
1618        let validator = TransactionValidator::new(fee_payer).unwrap();
1619        let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority);
1620        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1621        let mut transaction =
1622            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1623        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_err());
1624    }
1625
1626    #[test]
1627    #[serial]
1628    fn test_strict_pricing_total_exceeds_fixed() {
1629        let mut config = ConfigMockBuilder::new().build();
1630        config.validation.price.model = PriceModel::Fixed {
1631            amount: 5000,
1632            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1633            strict: true,
1634        };
1635        let _ = update_config(config);
1636
1637        // Fixed price = 5000, but total = 3000 + 2000 + 5000 = 10000 > 5000
1638        let fee_calc = TotalFeeCalculation::new(5000, 3000, 2000, 5000, 0, 0);
1639
1640        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1641
1642        assert!(result.is_err());
1643        if let Err(KoraError::ValidationError(msg)) = result {
1644            assert!(msg.contains("Strict pricing violation"));
1645            assert!(msg.contains("exceeds fixed price"));
1646        } else {
1647            panic!("Expected ValidationError");
1648        }
1649    }
1650
1651    #[test]
1652    #[serial]
1653    fn test_strict_pricing_total_within_fixed() {
1654        let mut config = ConfigMockBuilder::new().build();
1655        config.validation.price.model = PriceModel::Fixed {
1656            amount: 5000,
1657            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1658            strict: true,
1659        };
1660        let _ = update_config(config);
1661
1662        // Fixed price = 5000, total = 1000 + 1000 + 1000 = 3000 < 5000
1663        let fee_calc = TotalFeeCalculation::new(5000, 1000, 1000, 1000, 0, 0);
1664
1665        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1666
1667        assert!(result.is_ok());
1668    }
1669
1670    #[test]
1671    #[serial]
1672    fn test_strict_pricing_disabled() {
1673        let mut config = ConfigMockBuilder::new().build();
1674        config.validation.price.model = PriceModel::Fixed {
1675            amount: 5000,
1676            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1677            strict: false, // Disabled
1678        };
1679        let _ = update_config(config);
1680
1681        let fee_calc = TotalFeeCalculation::new(5000, 10000, 0, 0, 0, 0);
1682
1683        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1684
1685        assert!(result.is_ok(), "Should pass when strict=false");
1686    }
1687
1688    #[test]
1689    #[serial]
1690    fn test_strict_pricing_with_margin_pricing() {
1691        use crate::{
1692            fee::price::PriceModel, state::update_config, tests::config_mock::ConfigMockBuilder,
1693        };
1694
1695        let mut config = ConfigMockBuilder::new().build();
1696        config.validation.price.model = PriceModel::Margin { margin: 0.1 };
1697        let _ = update_config(config);
1698
1699        let fee_calc = TotalFeeCalculation::new(5000, 10000, 0, 0, 0, 0);
1700
1701        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1702
1703        assert!(result.is_ok());
1704    }
1705
1706    #[test]
1707    #[serial]
1708    fn test_strict_pricing_exact_match() {
1709        use crate::{
1710            fee::price::PriceModel, state::update_config, tests::config_mock::ConfigMockBuilder,
1711        };
1712
1713        let mut config = ConfigMockBuilder::new().build();
1714        config.validation.price.model = PriceModel::Fixed {
1715            amount: 5000,
1716            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1717            strict: true,
1718        };
1719        let _ = update_config(config);
1720
1721        // Total exactly equals fixed price (5000 = 5000)
1722        let fee_calc = TotalFeeCalculation::new(5000, 2000, 1000, 2000, 0, 0);
1723
1724        let result = TransactionValidator::validate_strict_pricing_with_fee(&fee_calc);
1725
1726        assert!(result.is_ok(), "Should pass when total equals fixed price");
1727    }
1728
1729    #[tokio::test]
1730    #[serial]
1731    async fn test_durable_transaction_rejected_by_default() {
1732        use solana_system_interface::instruction::advance_nonce_account;
1733
1734        let fee_payer = Pubkey::new_unique();
1735        let nonce_account = Pubkey::new_unique();
1736        let nonce_authority = Pubkey::new_unique(); // Different from fee payer
1737
1738        // Default config has allow_durable_transactions = false
1739        setup_default_config();
1740        let rpc_client = RpcMockBuilder::new().build();
1741
1742        let validator = TransactionValidator::new(fee_payer).unwrap();
1743
1744        // Transaction with AdvanceNonceAccount (authority is NOT fee payer)
1745        let instruction = advance_nonce_account(&nonce_account, &nonce_authority);
1746        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1747        let mut transaction =
1748            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1749
1750        let result = validator.validate_transaction(&mut transaction, &rpc_client).await;
1751        assert!(result.is_err());
1752        if let Err(KoraError::InvalidTransaction(msg)) = result {
1753            assert!(msg.contains("Durable transactions"));
1754            assert!(msg.contains("not allowed"));
1755        } else {
1756            panic!("Expected InvalidTransaction error");
1757        }
1758    }
1759
1760    #[tokio::test]
1761    #[serial]
1762    async fn test_durable_transaction_allowed_when_enabled() {
1763        use solana_system_interface::instruction::advance_nonce_account;
1764
1765        let fee_payer = Pubkey::new_unique();
1766        let nonce_account = Pubkey::new_unique();
1767        let nonce_authority = Pubkey::new_unique(); // Different from fee payer
1768
1769        // Enable durable transactions
1770        let config = ConfigMockBuilder::new()
1771            .with_price_source(PriceSource::Mock)
1772            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
1773            .with_max_allowed_lamports(1_000_000)
1774            .with_fee_payer_policy(FeePayerPolicy::default())
1775            .with_allow_durable_transactions(true)
1776            .build();
1777        update_config(config).unwrap();
1778
1779        let rpc_client = RpcMockBuilder::new().build();
1780        let validator = TransactionValidator::new(fee_payer).unwrap();
1781
1782        // Transaction with AdvanceNonceAccount (authority is NOT fee payer)
1783        let instruction = advance_nonce_account(&nonce_account, &nonce_authority);
1784        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1785        let mut transaction =
1786            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1787
1788        // Should pass because durable transactions are allowed
1789        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1790    }
1791
1792    #[tokio::test]
1793    #[serial]
1794    async fn test_non_durable_transaction_passes() {
1795        let fee_payer = Pubkey::new_unique();
1796        let sender = Pubkey::new_unique();
1797        let recipient = Pubkey::new_unique();
1798
1799        // Default config has allow_durable_transactions = false
1800        setup_default_config();
1801        let rpc_client = RpcMockBuilder::new().build();
1802
1803        let validator = TransactionValidator::new(fee_payer).unwrap();
1804
1805        // Regular transfer (no nonce instruction)
1806        let instruction = transfer(&sender, &recipient, 1000);
1807        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1808        let mut transaction =
1809            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1810
1811        // Should pass - no AdvanceNonceAccount instruction
1812        assert!(validator.validate_transaction(&mut transaction, &rpc_client).await.is_ok());
1813    }
1814}