Skip to main content

kora_lib/validator/
transaction_validator.rs

1use crate::{
2    config::{Config, FeePayerPolicy},
3    error::KoraError,
4    fee::fee::{FeeConfigUtil, TotalFeeCalculation},
5    oracle::PriceSource,
6    token::{interface::TokenMint, token::TokenUtil},
7    transaction::{
8        ParsedSPLInstructionData, ParsedSPLInstructionType, ParsedSystemInstructionData,
9        ParsedSystemInstructionType, VersionedTransactionResolved,
10    },
11};
12use solana_client::nonblocking::rpc_client::RpcClient;
13use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction};
14use std::str::FromStr;
15
16use crate::fee::price::PriceModel;
17
18pub struct TransactionValidator {
19    fee_payer_pubkey: Pubkey,
20    max_allowed_lamports: u64,
21    allowed_programs: Vec<Pubkey>,
22    max_signatures: u64,
23    allowed_tokens: Vec<Pubkey>,
24    disallowed_accounts: Vec<Pubkey>,
25    _price_source: PriceSource,
26    fee_payer_policy: FeePayerPolicy,
27    allow_durable_transactions: bool,
28}
29
30impl TransactionValidator {
31    pub fn new(config: &Config, fee_payer_pubkey: Pubkey) -> Result<Self, KoraError> {
32        let config = &config.validation;
33
34        // Convert string program IDs to Pubkeys
35        let allowed_programs = config
36            .allowed_programs
37            .iter()
38            .map(|addr| {
39                Pubkey::from_str(addr).map_err(|e| {
40                    KoraError::InternalServerError(format!(
41                        "Invalid program address in config: {e}"
42                    ))
43                })
44            })
45            .collect::<Result<Vec<Pubkey>, KoraError>>()?;
46
47        Ok(Self {
48            fee_payer_pubkey,
49            max_allowed_lamports: config.max_allowed_lamports,
50            allowed_programs,
51            max_signatures: config.max_signatures,
52            _price_source: config.price_source.clone(),
53            allowed_tokens: config
54                .allowed_tokens
55                .iter()
56                .map(|addr| Pubkey::from_str(addr))
57                .collect::<Result<Vec<Pubkey>, _>>()
58                .map_err(|e| {
59                    KoraError::InternalServerError(format!("Invalid allowed token address: {e}"))
60                })?,
61            disallowed_accounts: config
62                .disallowed_accounts
63                .iter()
64                .map(|addr| Pubkey::from_str(addr))
65                .collect::<Result<Vec<Pubkey>, _>>()
66                .map_err(|e| {
67                    KoraError::InternalServerError(format!(
68                        "Invalid disallowed account address: {e}"
69                    ))
70                })?,
71            fee_payer_policy: config.fee_payer_policy.clone(),
72            allow_durable_transactions: config.allow_durable_transactions,
73        })
74    }
75
76    pub async fn fetch_and_validate_token_mint(
77        &self,
78        mint: &Pubkey,
79        config: &Config,
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(config, 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        config: &Config,
100        transaction_resolved: &mut VersionedTransactionResolved,
101        rpc_client: &RpcClient,
102    ) -> Result<(), KoraError> {
103        if transaction_resolved.all_instructions.is_empty() {
104            return Err(KoraError::InvalidTransaction(
105                "Transaction contains no instructions".to_string(),
106            ));
107        }
108
109        if transaction_resolved.all_account_keys.is_empty() {
110            return Err(KoraError::InvalidTransaction(
111                "Transaction contains no account keys".to_string(),
112            ));
113        }
114
115        self.validate_signatures(&transaction_resolved.transaction)?;
116
117        self.validate_programs(transaction_resolved)?;
118        self.validate_transfer_amounts(config, transaction_resolved, rpc_client).await?;
119        self.validate_disallowed_accounts(transaction_resolved)?;
120        self.validate_fee_payer_usage(transaction_resolved)?;
121
122        Ok(())
123    }
124
125    pub fn validate_lamport_fee(&self, fee: u64) -> Result<(), KoraError> {
126        if fee > self.max_allowed_lamports {
127            return Err(KoraError::InvalidTransaction(format!(
128                "Fee {} exceeds maximum allowed {}",
129                fee, self.max_allowed_lamports
130            )));
131        }
132        Ok(())
133    }
134
135    fn validate_signatures(&self, transaction: &VersionedTransaction) -> Result<(), KoraError> {
136        if transaction.signatures.len() > self.max_signatures as usize {
137            return Err(KoraError::InvalidTransaction(format!(
138                "Too many signatures: {} > {}",
139                transaction.signatures.len(),
140                self.max_signatures
141            )));
142        }
143
144        if transaction.signatures.is_empty() {
145            return Err(KoraError::InvalidTransaction("No signatures found".to_string()));
146        }
147
148        Ok(())
149    }
150
151    fn validate_programs(
152        &self,
153        transaction_resolved: &VersionedTransactionResolved,
154    ) -> Result<(), KoraError> {
155        for instruction in &transaction_resolved.all_instructions {
156            if !self.allowed_programs.contains(&instruction.program_id) {
157                return Err(KoraError::InvalidTransaction(format!(
158                    "Program {} is not in the allowed list",
159                    instruction.program_id
160                )));
161            }
162        }
163        Ok(())
164    }
165
166    fn validate_fee_payer_usage(
167        &self,
168        transaction_resolved: &mut VersionedTransactionResolved,
169    ) -> Result<(), KoraError> {
170        let system_instructions = transaction_resolved.get_or_parse_system_instructions()?;
171
172        // Check for durable transactions (nonce-based) - reject if not allowed
173        if !self.allow_durable_transactions
174            && system_instructions
175                .contains_key(&ParsedSystemInstructionType::SystemAdvanceNonceAccount)
176        {
177            return Err(KoraError::InvalidTransaction(
178                "Durable transactions (nonce-based) are not allowed".to_string(),
179            ));
180        }
181
182        // Validate system program instructions
183        validate_system!(self, system_instructions, SystemTransfer,
184            ParsedSystemInstructionData::SystemTransfer { sender, .. } => sender,
185            self.fee_payer_policy.system.allow_transfer, "System Transfer");
186
187        validate_system!(self, system_instructions, SystemAssign,
188            ParsedSystemInstructionData::SystemAssign { authority } => authority,
189            self.fee_payer_policy.system.allow_assign, "System Assign");
190
191        validate_system!(self, system_instructions, SystemAllocate,
192            ParsedSystemInstructionData::SystemAllocate { account } => account,
193            self.fee_payer_policy.system.allow_allocate, "System Allocate");
194
195        validate_system!(self, system_instructions, SystemCreateAccount,
196            ParsedSystemInstructionData::SystemCreateAccount { payer, .. } => payer,
197            self.fee_payer_policy.system.allow_create_account, "System Create Account");
198
199        validate_system!(self, system_instructions, SystemInitializeNonceAccount,
200            ParsedSystemInstructionData::SystemInitializeNonceAccount { nonce_authority, .. } => nonce_authority,
201            self.fee_payer_policy.system.nonce.allow_initialize, "System Initialize Nonce Account");
202
203        validate_system!(self, system_instructions, SystemAdvanceNonceAccount,
204            ParsedSystemInstructionData::SystemAdvanceNonceAccount { nonce_authority, .. } => nonce_authority,
205            self.fee_payer_policy.system.nonce.allow_advance, "System Advance Nonce Account");
206
207        validate_system!(self, system_instructions, SystemAuthorizeNonceAccount,
208            ParsedSystemInstructionData::SystemAuthorizeNonceAccount { nonce_authority, .. } => nonce_authority,
209            self.fee_payer_policy.system.nonce.allow_authorize, "System Authorize Nonce Account");
210
211        // Note: SystemUpgradeNonceAccount not validated - no authority parameter
212
213        validate_system!(self, system_instructions, SystemWithdrawNonceAccount,
214            ParsedSystemInstructionData::SystemWithdrawNonceAccount { nonce_authority, .. } => nonce_authority,
215            self.fee_payer_policy.system.nonce.allow_withdraw, "System Withdraw Nonce Account");
216
217        // Validate SPL instructions
218        let spl_instructions = transaction_resolved.get_or_parse_spl_instructions()?;
219
220        validate_spl!(self, spl_instructions, SplTokenTransfer,
221            ParsedSPLInstructionData::SplTokenTransfer { owner, is_2022, .. } => { owner, is_2022 },
222            self.fee_payer_policy.spl_token.allow_transfer,
223            self.fee_payer_policy.token_2022.allow_transfer,
224            "SPL Token Transfer", "Token2022 Token Transfer");
225
226        validate_spl!(self, spl_instructions, SplTokenApprove,
227            ParsedSPLInstructionData::SplTokenApprove { owner, is_2022, .. } => { owner, is_2022 },
228            self.fee_payer_policy.spl_token.allow_approve,
229            self.fee_payer_policy.token_2022.allow_approve,
230            "SPL Token Approve", "Token2022 Token Approve");
231
232        validate_spl!(self, spl_instructions, SplTokenBurn,
233            ParsedSPLInstructionData::SplTokenBurn { owner, is_2022 } => { owner, is_2022 },
234            self.fee_payer_policy.spl_token.allow_burn,
235            self.fee_payer_policy.token_2022.allow_burn,
236            "SPL Token Burn", "Token2022 Token Burn");
237
238        validate_spl!(self, spl_instructions, SplTokenCloseAccount,
239            ParsedSPLInstructionData::SplTokenCloseAccount { owner, is_2022 } => { owner, is_2022 },
240            self.fee_payer_policy.spl_token.allow_close_account,
241            self.fee_payer_policy.token_2022.allow_close_account,
242            "SPL Token Close Account", "Token2022 Token Close Account");
243
244        validate_spl!(self, spl_instructions, SplTokenRevoke,
245            ParsedSPLInstructionData::SplTokenRevoke { owner, is_2022 } => { owner, is_2022 },
246            self.fee_payer_policy.spl_token.allow_revoke,
247            self.fee_payer_policy.token_2022.allow_revoke,
248            "SPL Token Revoke", "Token2022 Token Revoke");
249
250        validate_spl!(self, spl_instructions, SplTokenSetAuthority,
251            ParsedSPLInstructionData::SplTokenSetAuthority { authority, is_2022 } => { authority, is_2022 },
252            self.fee_payer_policy.spl_token.allow_set_authority,
253            self.fee_payer_policy.token_2022.allow_set_authority,
254            "SPL Token SetAuthority", "Token2022 Token SetAuthority");
255
256        validate_spl!(self, spl_instructions, SplTokenMintTo,
257            ParsedSPLInstructionData::SplTokenMintTo { mint_authority, is_2022 } => { mint_authority, is_2022 },
258            self.fee_payer_policy.spl_token.allow_mint_to,
259            self.fee_payer_policy.token_2022.allow_mint_to,
260            "SPL Token MintTo", "Token2022 Token MintTo");
261
262        validate_spl!(self, spl_instructions, SplTokenInitializeMint,
263            ParsedSPLInstructionData::SplTokenInitializeMint { mint_authority, is_2022 } => { mint_authority, is_2022 },
264            self.fee_payer_policy.spl_token.allow_initialize_mint,
265            self.fee_payer_policy.token_2022.allow_initialize_mint,
266            "SPL Token InitializeMint", "Token2022 Token InitializeMint");
267
268        validate_spl!(self, spl_instructions, SplTokenInitializeAccount,
269            ParsedSPLInstructionData::SplTokenInitializeAccount { owner, is_2022 } => { owner, is_2022 },
270            self.fee_payer_policy.spl_token.allow_initialize_account,
271            self.fee_payer_policy.token_2022.allow_initialize_account,
272            "SPL Token InitializeAccount", "Token2022 Token InitializeAccount");
273
274        validate_spl_multisig!(self, spl_instructions, SplTokenInitializeMultisig,
275            ParsedSPLInstructionData::SplTokenInitializeMultisig { signers, is_2022 } => { signers, is_2022 },
276            self.fee_payer_policy.spl_token.allow_initialize_multisig,
277            self.fee_payer_policy.token_2022.allow_initialize_multisig,
278            "SPL Token InitializeMultisig", "Token2022 Token InitializeMultisig");
279
280        validate_spl!(self, spl_instructions, SplTokenFreezeAccount,
281            ParsedSPLInstructionData::SplTokenFreezeAccount { freeze_authority, is_2022 } => { freeze_authority, is_2022 },
282            self.fee_payer_policy.spl_token.allow_freeze_account,
283            self.fee_payer_policy.token_2022.allow_freeze_account,
284            "SPL Token FreezeAccount", "Token2022 Token FreezeAccount");
285
286        validate_spl!(self, spl_instructions, SplTokenThawAccount,
287            ParsedSPLInstructionData::SplTokenThawAccount { freeze_authority, is_2022 } => { freeze_authority, is_2022 },
288            self.fee_payer_policy.spl_token.allow_thaw_account,
289            self.fee_payer_policy.token_2022.allow_thaw_account,
290            "SPL Token ThawAccount", "Token2022 Token ThawAccount");
291
292        Ok(())
293    }
294
295    async fn validate_transfer_amounts(
296        &self,
297        config: &Config,
298        transaction_resolved: &mut VersionedTransactionResolved,
299        rpc_client: &RpcClient,
300    ) -> Result<(), KoraError> {
301        let total_outflow =
302            self.calculate_total_outflow(config, transaction_resolved, rpc_client).await?;
303
304        if total_outflow > self.max_allowed_lamports {
305            return Err(KoraError::InvalidTransaction(format!(
306                "Total transfer amount {} exceeds maximum allowed {}",
307                total_outflow, self.max_allowed_lamports
308            )));
309        }
310
311        Ok(())
312    }
313
314    fn validate_disallowed_accounts(
315        &self,
316        transaction_resolved: &VersionedTransactionResolved,
317    ) -> Result<(), KoraError> {
318        for instruction in &transaction_resolved.all_instructions {
319            if self.disallowed_accounts.contains(&instruction.program_id) {
320                return Err(KoraError::InvalidTransaction(format!(
321                    "Program {} is disallowed",
322                    instruction.program_id
323                )));
324            }
325
326            for account_index in instruction.accounts.iter() {
327                if self.disallowed_accounts.contains(&account_index.pubkey) {
328                    return Err(KoraError::InvalidTransaction(format!(
329                        "Account {} is disallowed",
330                        account_index.pubkey
331                    )));
332                }
333            }
334        }
335        Ok(())
336    }
337
338    pub fn is_disallowed_account(&self, account: &Pubkey) -> bool {
339        self.disallowed_accounts.contains(account)
340    }
341
342    async fn calculate_total_outflow(
343        &self,
344        config: &Config,
345        transaction_resolved: &mut VersionedTransactionResolved,
346        rpc_client: &RpcClient,
347    ) -> Result<u64, KoraError> {
348        FeeConfigUtil::calculate_fee_payer_outflow(
349            &self.fee_payer_pubkey,
350            transaction_resolved,
351            rpc_client,
352            config,
353        )
354        .await
355    }
356
357    pub async fn validate_token_payment(
358        config: &Config,
359        transaction_resolved: &mut VersionedTransactionResolved,
360        required_lamports: u64,
361        rpc_client: &RpcClient,
362        expected_payment_destination: &Pubkey,
363    ) -> Result<(), KoraError> {
364        if TokenUtil::verify_token_payment(
365            config,
366            transaction_resolved,
367            rpc_client,
368            required_lamports,
369            expected_payment_destination,
370            None,
371        )
372        .await?
373        {
374            return Ok(());
375        }
376
377        Err(KoraError::InvalidTransaction(format!(
378            "Insufficient token payment. Required {required_lamports} lamports"
379        )))
380    }
381
382    pub fn validate_strict_pricing_with_fee(
383        config: &Config,
384        fee_calculation: &TotalFeeCalculation,
385    ) -> Result<(), KoraError> {
386        if !matches!(&config.validation.price.model, PriceModel::Fixed { strict: true, .. }) {
387            return Ok(());
388        }
389
390        let fixed_price_lamports = fee_calculation.total_fee_lamports;
391        let total_fee_lamports = fee_calculation.get_total_fee_lamports()?;
392
393        if fixed_price_lamports < total_fee_lamports {
394            log::error!(
395                "Strict pricing violation: fixed_price_lamports={} < total_fee_lamports={}",
396                fixed_price_lamports,
397                total_fee_lamports
398            );
399            return Err(KoraError::ValidationError(format!(
400                    "Strict pricing violation: total fee ({} lamports) exceeds fixed price ({} lamports)",
401                    total_fee_lamports,
402                    fixed_price_lamports
403                )));
404        }
405
406        Ok(())
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use crate::{
413        config::{Config, FeePayerPolicy},
414        state::{get_config, update_config},
415        tests::{
416            account_mock::{MintAccountMockBuilder, TokenAccountMockBuilder},
417            config_mock::{mock_state::setup_config_mock, ConfigMockBuilder},
418            rpc_mock::RpcMockBuilder,
419        },
420        transaction::TransactionUtil,
421    };
422    use serial_test::serial;
423
424    use super::*;
425    use solana_message::{Message, VersionedMessage};
426    use solana_sdk::instruction::Instruction;
427    use solana_system_interface::{
428        instruction::{
429            assign, create_account, create_account_with_seed, transfer, transfer_with_seed,
430        },
431        program::ID as SYSTEM_PROGRAM_ID,
432    };
433
434    fn setup_both_configs(config: Config) {
435        drop(setup_config_mock(config.clone()));
436        update_config(config).unwrap();
437    }
438
439    // Helper functions to reduce test duplication and setup config
440    fn setup_default_config() {
441        let config = ConfigMockBuilder::new()
442            .with_price_source(PriceSource::Mock)
443            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
444            .with_max_allowed_lamports(1_000_000)
445            .with_fee_payer_policy(FeePayerPolicy::default())
446            .build();
447        setup_both_configs(config);
448    }
449
450    fn setup_config_with_policy(policy: FeePayerPolicy) {
451        let config = ConfigMockBuilder::new()
452            .with_price_source(PriceSource::Mock)
453            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
454            .with_max_allowed_lamports(1_000_000)
455            .with_fee_payer_policy(policy)
456            .build();
457        setup_both_configs(config);
458    }
459
460    fn setup_spl_config_with_policy(policy: FeePayerPolicy) {
461        let config = ConfigMockBuilder::new()
462            .with_price_source(PriceSource::Mock)
463            .with_allowed_programs(vec![spl_token_interface::id().to_string()])
464            .with_max_allowed_lamports(1_000_000)
465            .with_fee_payer_policy(policy)
466            .build();
467        setup_both_configs(config);
468    }
469
470    fn setup_token2022_config_with_policy(policy: FeePayerPolicy) {
471        let config = ConfigMockBuilder::new()
472            .with_price_source(PriceSource::Mock)
473            .with_allowed_programs(vec![spl_token_2022_interface::id().to_string()])
474            .with_max_allowed_lamports(1_000_000)
475            .with_fee_payer_policy(policy)
476            .build();
477        setup_both_configs(config);
478    }
479
480    #[tokio::test]
481    #[serial]
482    async fn test_validate_transaction() {
483        let fee_payer = Pubkey::new_unique();
484        setup_default_config();
485        let rpc_client = RpcMockBuilder::new().build();
486
487        let config = get_config().unwrap();
488        let validator = TransactionValidator::new(config, fee_payer).unwrap();
489
490        let recipient = Pubkey::new_unique();
491        let sender = Pubkey::new_unique();
492        let instruction = transfer(&sender, &recipient, 100_000);
493        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
494        let mut transaction =
495            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
496
497        assert!(validator
498            .validate_transaction(config, &mut transaction, &rpc_client)
499            .await
500            .is_ok());
501    }
502
503    #[tokio::test]
504    #[serial]
505    async fn test_transfer_amount_limits() {
506        let fee_payer = Pubkey::new_unique();
507        setup_default_config();
508        let rpc_client = RpcMockBuilder::new().build();
509
510        let config = get_config().unwrap();
511        let validator = TransactionValidator::new(config, fee_payer).unwrap();
512        let sender = Pubkey::new_unique();
513        let recipient = Pubkey::new_unique();
514
515        // Test transaction with amount over limit
516        let instruction = transfer(&sender, &recipient, 2_000_000);
517        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
518        let mut transaction =
519            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
520
521        assert!(validator
522            .validate_transaction(config, &mut transaction, &rpc_client)
523            .await
524            .is_ok());
525
526        // Test multiple transfers
527        let instructions =
528            vec![transfer(&sender, &recipient, 500_000), transfer(&sender, &recipient, 500_000)];
529        let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
530        let mut transaction =
531            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
532        assert!(validator
533            .validate_transaction(config, &mut transaction, &rpc_client)
534            .await
535            .is_ok());
536    }
537
538    #[tokio::test]
539    #[serial]
540    async fn test_validate_programs() {
541        let fee_payer = Pubkey::new_unique();
542        setup_default_config();
543        let rpc_client = RpcMockBuilder::new().build();
544
545        let config = get_config().unwrap();
546        let validator = TransactionValidator::new(config, fee_payer).unwrap();
547        let sender = Pubkey::new_unique();
548        let recipient = Pubkey::new_unique();
549
550        // Test allowed program (system program)
551        let instruction = transfer(&sender, &recipient, 1000);
552        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
553        let mut transaction =
554            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
555        assert!(validator
556            .validate_transaction(config, &mut transaction, &rpc_client)
557            .await
558            .is_ok());
559
560        // Test disallowed program
561        let fake_program = Pubkey::new_unique();
562        // Create a no-op instruction for the fake program
563        let instruction = Instruction::new_with_bincode(
564            fake_program,
565            &[0u8],
566            vec![], // no accounts needed for this test
567        );
568        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
569        let mut transaction =
570            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
571        assert!(validator
572            .validate_transaction(config, &mut transaction, &rpc_client)
573            .await
574            .is_err());
575    }
576
577    #[tokio::test]
578    #[serial]
579    async fn test_validate_signatures() {
580        let fee_payer = Pubkey::new_unique();
581        let config = ConfigMockBuilder::new()
582            .with_price_source(PriceSource::Mock)
583            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
584            .with_max_allowed_lamports(1_000_000)
585            .with_max_signatures(2)
586            .with_fee_payer_policy(FeePayerPolicy::default())
587            .build();
588        update_config(config).unwrap();
589
590        let rpc_client = RpcMockBuilder::new().build();
591        let config = get_config().unwrap();
592        let validator = TransactionValidator::new(config, fee_payer).unwrap();
593        let sender = Pubkey::new_unique();
594        let recipient = Pubkey::new_unique();
595
596        // Test too many signatures
597        let instructions = vec![
598            transfer(&sender, &recipient, 1000),
599            transfer(&sender, &recipient, 1000),
600            transfer(&sender, &recipient, 1000),
601        ];
602        let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
603        let mut transaction =
604            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
605        transaction.transaction.signatures = vec![Default::default(); 3]; // Add 3 dummy signatures
606        assert!(validator
607            .validate_transaction(config, &mut transaction, &rpc_client)
608            .await
609            .is_err());
610    }
611
612    #[tokio::test]
613    #[serial]
614    async fn test_sign_and_send_transaction_mode() {
615        let fee_payer = Pubkey::new_unique();
616        setup_default_config();
617        let rpc_client = RpcMockBuilder::new().build();
618
619        let config = get_config().unwrap();
620        let validator = TransactionValidator::new(config, fee_payer).unwrap();
621        let sender = Pubkey::new_unique();
622        let recipient = Pubkey::new_unique();
623
624        // Test SignAndSend mode with fee payer already set should not error
625        let instruction = transfer(&sender, &recipient, 1000);
626        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
627        let mut transaction =
628            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
629        assert!(validator
630            .validate_transaction(config, &mut transaction, &rpc_client)
631            .await
632            .is_ok());
633
634        // Test SignAndSend mode without fee payer (should succeed)
635        let instruction = transfer(&sender, &recipient, 1000);
636        let message = VersionedMessage::Legacy(Message::new(&[instruction], None)); // No fee payer specified
637        let mut transaction =
638            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
639        assert!(validator
640            .validate_transaction(config, &mut transaction, &rpc_client)
641            .await
642            .is_ok());
643    }
644
645    #[tokio::test]
646    #[serial]
647    async fn test_empty_transaction() {
648        let fee_payer = Pubkey::new_unique();
649        setup_default_config();
650        let rpc_client = RpcMockBuilder::new().build();
651
652        let config = get_config().unwrap();
653        let validator = TransactionValidator::new(config, fee_payer).unwrap();
654
655        // Create an empty message using Message::new with empty instructions
656        let message = VersionedMessage::Legacy(Message::new(&[], Some(&fee_payer)));
657        let mut transaction =
658            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
659        assert!(validator
660            .validate_transaction(config, &mut transaction, &rpc_client)
661            .await
662            .is_err());
663    }
664
665    #[tokio::test]
666    #[serial]
667    async fn test_disallowed_accounts() {
668        let fee_payer = Pubkey::new_unique();
669        let config = ConfigMockBuilder::new()
670            .with_price_source(PriceSource::Mock)
671            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
672            .with_max_allowed_lamports(1_000_000)
673            .with_disallowed_accounts(vec![
674                "hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek".to_string()
675            ])
676            .with_fee_payer_policy(FeePayerPolicy::default())
677            .build();
678        update_config(config).unwrap();
679
680        let rpc_client = RpcMockBuilder::new().build();
681        let config = get_config().unwrap();
682        let validator = TransactionValidator::new(config, fee_payer).unwrap();
683        let instruction = transfer(
684            &Pubkey::from_str("hndXZGK45hCxfBYvxejAXzCfCujoqkNf7rk4sTB8pek").unwrap(),
685            &fee_payer,
686            1000,
687        );
688        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
689        let mut transaction =
690            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
691        assert!(validator
692            .validate_transaction(config, &mut transaction, &rpc_client)
693            .await
694            .is_err());
695    }
696
697    #[tokio::test]
698    #[serial]
699    async fn test_fee_payer_policy_sol_transfers() {
700        let fee_payer = Pubkey::new_unique();
701        let recipient = Pubkey::new_unique();
702
703        // Test with allow_sol_transfers = true
704        let rpc_client = RpcMockBuilder::new().build();
705        let mut policy = FeePayerPolicy::default();
706        policy.system.allow_transfer = true;
707        setup_config_with_policy(policy);
708
709        let config = get_config().unwrap();
710        let validator = TransactionValidator::new(config, fee_payer).unwrap();
711
712        let instruction = transfer(&fee_payer, &recipient, 1000);
713
714        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
715        let mut transaction =
716            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
717        assert!(validator
718            .validate_transaction(config, &mut transaction, &rpc_client)
719            .await
720            .is_ok());
721
722        // Test with allow_sol_transfers = false
723        let rpc_client = RpcMockBuilder::new().build();
724        let mut policy = FeePayerPolicy::default();
725        policy.system.allow_transfer = false;
726        setup_config_with_policy(policy);
727
728        let config = get_config().unwrap();
729        let validator = TransactionValidator::new(config, fee_payer).unwrap();
730
731        let instruction = transfer(&fee_payer, &recipient, 1000);
732        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
733        let mut transaction =
734            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
735        assert!(validator
736            .validate_transaction(config, &mut transaction, &rpc_client)
737            .await
738            .is_err());
739    }
740
741    #[tokio::test]
742    #[serial]
743    async fn test_fee_payer_policy_assign() {
744        let fee_payer = Pubkey::new_unique();
745        let new_owner = Pubkey::new_unique();
746
747        // Test with allow_assign = true
748
749        let rpc_client = RpcMockBuilder::new().build();
750
751        let mut policy = FeePayerPolicy::default();
752        policy.system.allow_assign = true;
753        setup_config_with_policy(policy);
754
755        let config = get_config().unwrap();
756        let validator = TransactionValidator::new(config, fee_payer).unwrap();
757
758        let instruction = assign(&fee_payer, &new_owner);
759        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
760        let mut transaction =
761            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
762        assert!(validator
763            .validate_transaction(config, &mut transaction, &rpc_client)
764            .await
765            .is_ok());
766
767        // Test with allow_assign = false
768
769        let rpc_client = RpcMockBuilder::new().build();
770
771        let mut policy = FeePayerPolicy::default();
772        policy.system.allow_assign = false;
773        setup_config_with_policy(policy);
774
775        let config = get_config().unwrap();
776        let validator = TransactionValidator::new(config, fee_payer).unwrap();
777
778        let instruction = assign(&fee_payer, &new_owner);
779        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
780        let mut transaction =
781            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
782        assert!(validator
783            .validate_transaction(config, &mut transaction, &rpc_client)
784            .await
785            .is_err());
786    }
787
788    #[tokio::test]
789    #[serial]
790    async fn test_fee_payer_policy_spl_transfers() {
791        let fee_payer = Pubkey::new_unique();
792
793        let fee_payer_token_account = Pubkey::new_unique();
794        let recipient_token_account = Pubkey::new_unique();
795        let mint = Pubkey::new_unique();
796
797        let source_token_account =
798            TokenAccountMockBuilder::new().with_mint(&mint).with_owner(&fee_payer).build();
799        let mint_account = MintAccountMockBuilder::new().with_decimals(6).build();
800
801        // Test with allow_spl_transfers = true (plain Transfer, mint resolved from source account)
802        let rpc_client = RpcMockBuilder::new()
803            .build_with_sequential_accounts(vec![&source_token_account, &mint_account]);
804
805        let mut policy = FeePayerPolicy::default();
806        policy.spl_token.allow_transfer = true;
807        setup_spl_config_with_policy(policy);
808
809        let config = get_config().unwrap();
810        let validator = TransactionValidator::new(config, fee_payer).unwrap();
811
812        let transfer_ix = spl_token_interface::instruction::transfer(
813            &spl_token_interface::id(),
814            &fee_payer_token_account,
815            &recipient_token_account,
816            &fee_payer, // fee payer is the signer
817            &[],
818            1000,
819        )
820        .unwrap();
821
822        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
823        let mut transaction =
824            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
825        assert!(validator
826            .validate_transaction(config, &mut transaction, &rpc_client)
827            .await
828            .is_ok());
829
830        // Test with allow_spl_transfers = false
831        let rpc_client = RpcMockBuilder::new()
832            .build_with_sequential_accounts(vec![&source_token_account, &mint_account]);
833
834        let mut policy = FeePayerPolicy::default();
835        policy.spl_token.allow_transfer = false;
836        setup_spl_config_with_policy(policy);
837
838        let config = get_config().unwrap();
839        let validator = TransactionValidator::new(config, fee_payer).unwrap();
840
841        let transfer_ix = spl_token_interface::instruction::transfer(
842            &spl_token_interface::id(),
843            &fee_payer_token_account,
844            &recipient_token_account,
845            &fee_payer, // fee payer is the signer
846            &[],
847            1000,
848        )
849        .unwrap();
850
851        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
852        let mut transaction =
853            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
854        assert!(validator
855            .validate_transaction(config, &mut transaction, &rpc_client)
856            .await
857            .is_err());
858
859        // Test with other account as source - should always pass
860        let rpc_client = RpcMockBuilder::new().build();
861        let other_signer = Pubkey::new_unique();
862        let transfer_ix = spl_token_interface::instruction::transfer(
863            &spl_token_interface::id(),
864            &fee_payer_token_account,
865            &recipient_token_account,
866            &other_signer, // other account is the signer
867            &[],
868            1000,
869        )
870        .unwrap();
871
872        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
873        let mut transaction =
874            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
875        assert!(validator
876            .validate_transaction(config, &mut transaction, &rpc_client)
877            .await
878            .is_ok());
879    }
880
881    #[tokio::test]
882    #[serial]
883    async fn test_fee_payer_policy_token2022_transfers() {
884        let fee_payer = Pubkey::new_unique();
885
886        let fee_payer_token_account = Pubkey::new_unique();
887        let recipient_token_account = Pubkey::new_unique();
888        let mint = Pubkey::new_unique();
889
890        // Test with allow_token2022_transfers = true
891        let rpc_client = RpcMockBuilder::new()
892            .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation
893            .build();
894        // Test with token_2022.allow_transfer = true
895        let mut policy = FeePayerPolicy::default();
896        policy.token_2022.allow_transfer = true;
897        setup_token2022_config_with_policy(policy);
898
899        let config = get_config().unwrap();
900        let validator = TransactionValidator::new(config, fee_payer).unwrap();
901
902        let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
903            &spl_token_2022_interface::id(),
904            &fee_payer_token_account,
905            &mint,
906            &recipient_token_account,
907            &fee_payer, // fee payer is the signer
908            &[],
909            1,
910            2,
911        )
912        .unwrap();
913
914        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
915        let mut transaction =
916            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
917        assert!(validator
918            .validate_transaction(config, &mut transaction, &rpc_client)
919            .await
920            .is_ok());
921
922        // Test with allow_token2022_transfers = false
923        let rpc_client = RpcMockBuilder::new()
924            .with_mint_account(2) // Mock mint with 2 decimals for SPL outflow calculation
925            .build();
926        let mut policy = FeePayerPolicy::default();
927        policy.token_2022.allow_transfer = false;
928        setup_token2022_config_with_policy(policy);
929
930        let config = get_config().unwrap();
931        let validator = TransactionValidator::new(config, fee_payer).unwrap();
932
933        let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
934            &spl_token_2022_interface::id(),
935            &fee_payer_token_account,
936            &mint,
937            &recipient_token_account,
938            &fee_payer, // fee payer is the signer
939            &[],
940            1000,
941            2,
942        )
943        .unwrap();
944
945        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
946        let mut transaction =
947            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
948
949        // Should fail because fee payer is not allowed to be source
950        assert!(validator
951            .validate_transaction(config, &mut transaction, &rpc_client)
952            .await
953            .is_err());
954
955        // Test with other account as source - should always pass
956        let other_signer = Pubkey::new_unique();
957        let transfer_ix = spl_token_2022_interface::instruction::transfer_checked(
958            &spl_token_2022_interface::id(),
959            &fee_payer_token_account,
960            &mint,
961            &recipient_token_account,
962            &other_signer, // other account is the signer
963            &[],
964            1000,
965            2,
966        )
967        .unwrap();
968
969        let message = VersionedMessage::Legacy(Message::new(&[transfer_ix], Some(&fee_payer)));
970        let mut transaction =
971            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
972
973        // Should pass because fee payer is not the source
974        assert!(validator
975            .validate_transaction(config, &mut transaction, &rpc_client)
976            .await
977            .is_ok());
978    }
979
980    #[tokio::test]
981    #[serial]
982    async fn test_calculate_total_outflow() {
983        let fee_payer = Pubkey::new_unique();
984        let config = ConfigMockBuilder::new()
985            .with_price_source(PriceSource::Mock)
986            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
987            .with_max_allowed_lamports(10_000_000)
988            .with_fee_payer_policy(FeePayerPolicy::default())
989            .build();
990        update_config(config).unwrap();
991
992        let rpc_client = RpcMockBuilder::new().build();
993        let config = get_config().unwrap();
994        let validator = TransactionValidator::new(config, fee_payer).unwrap();
995
996        // Test 1: Fee payer as sender in Transfer - should add to outflow
997        let recipient = Pubkey::new_unique();
998        let transfer_instruction = transfer(&fee_payer, &recipient, 100_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(config, &mut transaction, &rpc_client).await.unwrap();
1005        assert_eq!(outflow, 100_000, "Transfer from fee payer should add to outflow");
1006
1007        // Test 2: Fee payer as recipient in Transfer - should subtract from outflow (account closure)
1008        let sender = Pubkey::new_unique();
1009        let transfer_instruction = transfer(&sender, &fee_payer, 50_000);
1010        let message =
1011            VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
1012        let mut transaction =
1013            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1014
1015        let outflow =
1016            validator.calculate_total_outflow(config, &mut transaction, &rpc_client).await.unwrap();
1017        assert_eq!(outflow, 0, "Transfer to fee payer should subtract from outflow"); // 0 - 50_000 = 0 (saturating_sub)
1018
1019        // Test 3: Fee payer as funding account in CreateAccount - should add to outflow
1020        let new_account = Pubkey::new_unique();
1021        let create_instruction = create_account(
1022            &fee_payer,
1023            &new_account,
1024            200_000, // lamports
1025            100,     // space
1026            &SYSTEM_PROGRAM_ID,
1027        );
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(config, &mut transaction, &rpc_client).await.unwrap();
1034        assert_eq!(outflow, 200_000, "CreateAccount funded by fee payer should add to outflow");
1035
1036        // Test 4: Fee payer as funding account in CreateAccountWithSeed - should add to outflow
1037        let create_with_seed_instruction = create_account_with_seed(
1038            &fee_payer,
1039            &new_account,
1040            &fee_payer,
1041            "test_seed",
1042            300_000, // lamports
1043            100,     // space
1044            &SYSTEM_PROGRAM_ID,
1045        );
1046        let message = VersionedMessage::Legacy(Message::new(
1047            &[create_with_seed_instruction],
1048            Some(&fee_payer),
1049        ));
1050        let mut transaction =
1051            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1052        let outflow =
1053            validator.calculate_total_outflow(config, &mut transaction, &rpc_client).await.unwrap();
1054        assert_eq!(
1055            outflow, 300_000,
1056            "CreateAccountWithSeed funded by fee payer should add to outflow"
1057        );
1058
1059        // Test 5: TransferWithSeed from fee payer - should add to outflow
1060        let transfer_with_seed_instruction = transfer_with_seed(
1061            &fee_payer,
1062            &fee_payer,
1063            "test_seed".to_string(),
1064            &SYSTEM_PROGRAM_ID,
1065            &recipient,
1066            150_000,
1067        );
1068        let message = VersionedMessage::Legacy(Message::new(
1069            &[transfer_with_seed_instruction],
1070            Some(&fee_payer),
1071        ));
1072        let mut transaction =
1073            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1074        let outflow =
1075            validator.calculate_total_outflow(config, &mut transaction, &rpc_client).await.unwrap();
1076        assert_eq!(outflow, 150_000, "TransferWithSeed from fee payer should add to outflow");
1077
1078        // Test 6: Multiple instructions - should sum correctly
1079        let instructions = vec![
1080            transfer(&fee_payer, &recipient, 100_000), // +100_000
1081            transfer(&sender, &fee_payer, 30_000),     // -30_000
1082            create_account(&fee_payer, &new_account, 50_000, 100, &SYSTEM_PROGRAM_ID), // +50_000
1083        ];
1084        let message = VersionedMessage::Legacy(Message::new(&instructions, Some(&fee_payer)));
1085        let mut transaction =
1086            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1087        let outflow =
1088            validator.calculate_total_outflow(config, &mut transaction, &rpc_client).await.unwrap();
1089        assert_eq!(
1090            outflow, 120_000,
1091            "Multiple instructions should sum correctly: 100000 - 30000 + 50000 = 120000"
1092        );
1093
1094        // Test 7: Other account as sender - should not affect outflow
1095        let other_sender = Pubkey::new_unique();
1096        let transfer_instruction = transfer(&other_sender, &recipient, 500_000);
1097        let message =
1098            VersionedMessage::Legacy(Message::new(&[transfer_instruction], Some(&fee_payer)));
1099        let mut transaction =
1100            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1101        let outflow =
1102            validator.calculate_total_outflow(config, &mut transaction, &rpc_client).await.unwrap();
1103        assert_eq!(outflow, 0, "Transfer from other account should not affect outflow");
1104
1105        // Test 8: Other account funding CreateAccount - should not affect outflow
1106        let other_funder = Pubkey::new_unique();
1107        let create_instruction =
1108            create_account(&other_funder, &new_account, 1_000_000, 100, &SYSTEM_PROGRAM_ID);
1109        let message =
1110            VersionedMessage::Legacy(Message::new(&[create_instruction], Some(&fee_payer)));
1111        let mut transaction =
1112            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1113        let outflow =
1114            validator.calculate_total_outflow(config, &mut transaction, &rpc_client).await.unwrap();
1115        assert_eq!(outflow, 0, "CreateAccount funded by other account should not affect outflow");
1116    }
1117
1118    #[tokio::test]
1119    #[serial]
1120    async fn test_fee_payer_policy_burn() {
1121        let fee_payer = Pubkey::new_unique();
1122        let fee_payer_token_account = Pubkey::new_unique();
1123        let mint = Pubkey::new_unique();
1124
1125        // Test with allow_burn = true
1126
1127        let rpc_client = RpcMockBuilder::new().build();
1128        let mut policy = FeePayerPolicy::default();
1129        policy.spl_token.allow_burn = true;
1130        setup_spl_config_with_policy(policy);
1131
1132        let config = get_config().unwrap();
1133        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1134
1135        let burn_ix = spl_token_interface::instruction::burn(
1136            &spl_token_interface::id(),
1137            &fee_payer_token_account,
1138            &mint,
1139            &fee_payer,
1140            &[],
1141            1000,
1142        )
1143        .unwrap();
1144
1145        let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer)));
1146        let mut transaction =
1147            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1148        // Should pass because allow_burn is true by default
1149        assert!(validator
1150            .validate_transaction(config, &mut transaction, &rpc_client)
1151            .await
1152            .is_ok());
1153
1154        // Test with allow_burn = false
1155
1156        let rpc_client = RpcMockBuilder::new().build();
1157        let mut policy = FeePayerPolicy::default();
1158        policy.spl_token.allow_burn = false;
1159        setup_spl_config_with_policy(policy);
1160
1161        let config = get_config().unwrap();
1162        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1163
1164        let burn_ix = spl_token_interface::instruction::burn(
1165            &spl_token_interface::id(),
1166            &fee_payer_token_account,
1167            &mint,
1168            &fee_payer,
1169            &[],
1170            1000,
1171        )
1172        .unwrap();
1173
1174        let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer)));
1175        let mut transaction =
1176            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1177
1178        // Should fail because fee payer cannot burn tokens when allow_burn is false
1179        assert!(validator
1180            .validate_transaction(config, &mut transaction, &rpc_client)
1181            .await
1182            .is_err());
1183
1184        // Test burn_checked instruction
1185        let burn_checked_ix = spl_token_interface::instruction::burn_checked(
1186            &spl_token_interface::id(),
1187            &fee_payer_token_account,
1188            &mint,
1189            &fee_payer,
1190            &[],
1191            1000,
1192            2,
1193        )
1194        .unwrap();
1195
1196        let message = VersionedMessage::Legacy(Message::new(&[burn_checked_ix], Some(&fee_payer)));
1197        let mut transaction =
1198            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1199
1200        // Should also fail for burn_checked
1201        assert!(validator
1202            .validate_transaction(config, &mut transaction, &rpc_client)
1203            .await
1204            .is_err());
1205    }
1206
1207    #[tokio::test]
1208    #[serial]
1209    async fn test_fee_payer_policy_close_account() {
1210        let fee_payer = Pubkey::new_unique();
1211        let fee_payer_token_account = Pubkey::new_unique();
1212        let destination = Pubkey::new_unique();
1213
1214        // Test with allow_close_account = true
1215
1216        let rpc_client = RpcMockBuilder::new().build();
1217        let mut policy = FeePayerPolicy::default();
1218        policy.spl_token.allow_close_account = true;
1219        setup_spl_config_with_policy(policy);
1220
1221        let config = get_config().unwrap();
1222        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1223
1224        let close_ix = spl_token_interface::instruction::close_account(
1225            &spl_token_interface::id(),
1226            &fee_payer_token_account,
1227            &destination,
1228            &fee_payer,
1229            &[],
1230        )
1231        .unwrap();
1232
1233        let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer)));
1234        let mut transaction =
1235            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1236        // Should pass because allow_close_account is true by default
1237        assert!(validator
1238            .validate_transaction(config, &mut transaction, &rpc_client)
1239            .await
1240            .is_ok());
1241
1242        // Test with allow_close_account = false
1243        let rpc_client = RpcMockBuilder::new().build();
1244        let mut policy = FeePayerPolicy::default();
1245        policy.spl_token.allow_close_account = false;
1246        setup_spl_config_with_policy(policy);
1247
1248        let config = get_config().unwrap();
1249        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1250
1251        let close_ix = spl_token_interface::instruction::close_account(
1252            &spl_token_interface::id(),
1253            &fee_payer_token_account,
1254            &destination,
1255            &fee_payer,
1256            &[],
1257        )
1258        .unwrap();
1259
1260        let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer)));
1261        let mut transaction =
1262            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1263
1264        // Should fail because fee payer cannot close accounts when allow_close_account is false
1265        assert!(validator
1266            .validate_transaction(config, &mut transaction, &rpc_client)
1267            .await
1268            .is_err());
1269    }
1270
1271    #[tokio::test]
1272    #[serial]
1273    async fn test_fee_payer_policy_approve() {
1274        let fee_payer = Pubkey::new_unique();
1275        let fee_payer_token_account = Pubkey::new_unique();
1276        let delegate = Pubkey::new_unique();
1277
1278        // Test with allow_approve = true
1279
1280        let rpc_client = RpcMockBuilder::new().build();
1281        let mut policy = FeePayerPolicy::default();
1282        policy.spl_token.allow_approve = true;
1283        setup_spl_config_with_policy(policy);
1284
1285        let config = get_config().unwrap();
1286        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1287
1288        let approve_ix = spl_token_interface::instruction::approve(
1289            &spl_token_interface::id(),
1290            &fee_payer_token_account,
1291            &delegate,
1292            &fee_payer,
1293            &[],
1294            1000,
1295        )
1296        .unwrap();
1297
1298        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1299        let mut transaction =
1300            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1301        // Should pass because allow_approve is true by default
1302        assert!(validator
1303            .validate_transaction(config, &mut transaction, &rpc_client)
1304            .await
1305            .is_ok());
1306
1307        // Test with allow_approve = false
1308        let rpc_client = RpcMockBuilder::new().build();
1309        let mut policy = FeePayerPolicy::default();
1310        policy.spl_token.allow_approve = false;
1311        setup_spl_config_with_policy(policy);
1312
1313        let config = get_config().unwrap();
1314        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1315
1316        let approve_ix = spl_token_interface::instruction::approve(
1317            &spl_token_interface::id(),
1318            &fee_payer_token_account,
1319            &delegate,
1320            &fee_payer,
1321            &[],
1322            1000,
1323        )
1324        .unwrap();
1325
1326        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1327        let mut transaction =
1328            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1329
1330        // Should fail because fee payer cannot approve when allow_approve is false
1331        assert!(validator
1332            .validate_transaction(config, &mut transaction, &rpc_client)
1333            .await
1334            .is_err());
1335
1336        // Test approve_checked instruction
1337        let mint = Pubkey::new_unique();
1338        let approve_checked_ix = spl_token_interface::instruction::approve_checked(
1339            &spl_token_interface::id(),
1340            &fee_payer_token_account,
1341            &mint,
1342            &delegate,
1343            &fee_payer,
1344            &[],
1345            1000,
1346            2,
1347        )
1348        .unwrap();
1349
1350        let message =
1351            VersionedMessage::Legacy(Message::new(&[approve_checked_ix], Some(&fee_payer)));
1352        let mut transaction =
1353            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1354
1355        // Should also fail for approve_checked
1356        assert!(validator
1357            .validate_transaction(config, &mut transaction, &rpc_client)
1358            .await
1359            .is_err());
1360    }
1361
1362    #[tokio::test]
1363    #[serial]
1364    async fn test_fee_payer_policy_token2022_burn() {
1365        let fee_payer = Pubkey::new_unique();
1366        let fee_payer_token_account = Pubkey::new_unique();
1367        let mint = Pubkey::new_unique();
1368
1369        // Test with allow_burn = false for Token2022
1370
1371        let rpc_client = RpcMockBuilder::new().build();
1372        let mut policy = FeePayerPolicy::default();
1373        policy.token_2022.allow_burn = false;
1374        setup_token2022_config_with_policy(policy);
1375
1376        let config = get_config().unwrap();
1377        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1378
1379        let burn_ix = spl_token_2022_interface::instruction::burn(
1380            &spl_token_2022_interface::id(),
1381            &fee_payer_token_account,
1382            &mint,
1383            &fee_payer,
1384            &[],
1385            1000,
1386        )
1387        .unwrap();
1388
1389        let message = VersionedMessage::Legacy(Message::new(&[burn_ix], Some(&fee_payer)));
1390        let mut transaction =
1391            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1392        // Should fail for Token2022 burn
1393        assert!(validator
1394            .validate_transaction(config, &mut transaction, &rpc_client)
1395            .await
1396            .is_err());
1397    }
1398
1399    #[tokio::test]
1400    #[serial]
1401    async fn test_fee_payer_policy_token2022_close_account() {
1402        let fee_payer = Pubkey::new_unique();
1403        let fee_payer_token_account = Pubkey::new_unique();
1404        let destination = Pubkey::new_unique();
1405
1406        // Test with allow_close_account = false for Token2022
1407
1408        let rpc_client = RpcMockBuilder::new().build();
1409        let mut policy = FeePayerPolicy::default();
1410        policy.token_2022.allow_close_account = false;
1411        setup_token2022_config_with_policy(policy);
1412
1413        let config = get_config().unwrap();
1414        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1415
1416        let close_ix = spl_token_2022_interface::instruction::close_account(
1417            &spl_token_2022_interface::id(),
1418            &fee_payer_token_account,
1419            &destination,
1420            &fee_payer,
1421            &[],
1422        )
1423        .unwrap();
1424
1425        let message = VersionedMessage::Legacy(Message::new(&[close_ix], Some(&fee_payer)));
1426        let mut transaction =
1427            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1428        // Should fail for Token2022 close account
1429        assert!(validator
1430            .validate_transaction(config, &mut transaction, &rpc_client)
1431            .await
1432            .is_err());
1433    }
1434
1435    #[tokio::test]
1436    #[serial]
1437    async fn test_fee_payer_policy_token2022_approve() {
1438        let fee_payer = Pubkey::new_unique();
1439        let fee_payer_token_account = Pubkey::new_unique();
1440        let delegate = Pubkey::new_unique();
1441
1442        // Test with allow_approve = true
1443
1444        let rpc_client = RpcMockBuilder::new().build();
1445        let mut policy = FeePayerPolicy::default();
1446        policy.token_2022.allow_approve = true;
1447        setup_token2022_config_with_policy(policy);
1448
1449        let config = get_config().unwrap();
1450        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1451
1452        let approve_ix = spl_token_2022_interface::instruction::approve(
1453            &spl_token_2022_interface::id(),
1454            &fee_payer_token_account,
1455            &delegate,
1456            &fee_payer,
1457            &[],
1458            1000,
1459        )
1460        .unwrap();
1461
1462        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1463        let mut transaction =
1464            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1465        // Should pass because allow_approve is true by default
1466        assert!(validator
1467            .validate_transaction(config, &mut transaction, &rpc_client)
1468            .await
1469            .is_ok());
1470
1471        // Test with allow_approve = false
1472
1473        let rpc_client = RpcMockBuilder::new().build();
1474        let mut policy = FeePayerPolicy::default();
1475        policy.token_2022.allow_approve = false;
1476        setup_token2022_config_with_policy(policy);
1477
1478        let config = get_config().unwrap();
1479        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1480
1481        let approve_ix = spl_token_2022_interface::instruction::approve(
1482            &spl_token_2022_interface::id(),
1483            &fee_payer_token_account,
1484            &delegate,
1485            &fee_payer,
1486            &[],
1487            1000,
1488        )
1489        .unwrap();
1490
1491        let message = VersionedMessage::Legacy(Message::new(&[approve_ix], Some(&fee_payer)));
1492        let mut transaction =
1493            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1494
1495        // Should fail because fee payer cannot approve when allow_approve is false
1496        assert!(validator
1497            .validate_transaction(config, &mut transaction, &rpc_client)
1498            .await
1499            .is_err());
1500
1501        // Test approve_checked instruction
1502        let mint = Pubkey::new_unique();
1503        let approve_checked_ix = spl_token_2022_interface::instruction::approve_checked(
1504            &spl_token_2022_interface::id(),
1505            &fee_payer_token_account,
1506            &mint,
1507            &delegate,
1508            &fee_payer,
1509            &[],
1510            1000,
1511            2,
1512        )
1513        .unwrap();
1514
1515        let message =
1516            VersionedMessage::Legacy(Message::new(&[approve_checked_ix], Some(&fee_payer)));
1517        let mut transaction =
1518            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1519
1520        // Should also fail for approve_checked
1521        assert!(validator
1522            .validate_transaction(config, &mut transaction, &rpc_client)
1523            .await
1524            .is_err());
1525    }
1526
1527    #[tokio::test]
1528    #[serial]
1529    async fn test_fee_payer_policy_create_account() {
1530        use solana_system_interface::instruction::create_account;
1531
1532        let fee_payer = Pubkey::new_unique();
1533        let new_account = Pubkey::new_unique();
1534        let owner = Pubkey::new_unique();
1535
1536        // Test with allow_create_account = true
1537        let rpc_client = RpcMockBuilder::new().build();
1538        let mut policy = FeePayerPolicy::default();
1539        policy.system.allow_create_account = true;
1540        setup_config_with_policy(policy);
1541
1542        let config = get_config().unwrap();
1543        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1544        let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner);
1545        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1546        let mut transaction =
1547            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1548        assert!(validator
1549            .validate_transaction(config, &mut transaction, &rpc_client)
1550            .await
1551            .is_ok());
1552
1553        // Test with allow_create_account = false
1554        let rpc_client = RpcMockBuilder::new().build();
1555        let mut policy = FeePayerPolicy::default();
1556        policy.system.allow_create_account = false;
1557        setup_config_with_policy(policy);
1558
1559        let config = get_config().unwrap();
1560        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1561        let instruction = create_account(&fee_payer, &new_account, 1000, 100, &owner);
1562        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1563        let mut transaction =
1564            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1565        assert!(validator
1566            .validate_transaction(config, &mut transaction, &rpc_client)
1567            .await
1568            .is_err());
1569    }
1570
1571    #[tokio::test]
1572    #[serial]
1573    async fn test_fee_payer_policy_allocate() {
1574        use solana_system_interface::instruction::allocate;
1575
1576        let fee_payer = Pubkey::new_unique();
1577
1578        // Test with allow_allocate = true
1579        let rpc_client = RpcMockBuilder::new().build();
1580        let mut policy = FeePayerPolicy::default();
1581        policy.system.allow_allocate = true;
1582        setup_config_with_policy(policy);
1583
1584        let config = get_config().unwrap();
1585        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1586        let instruction = allocate(&fee_payer, 100);
1587        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1588        let mut transaction =
1589            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1590        assert!(validator
1591            .validate_transaction(config, &mut transaction, &rpc_client)
1592            .await
1593            .is_ok());
1594
1595        // Test with allow_allocate = false
1596        let rpc_client = RpcMockBuilder::new().build();
1597        let mut policy = FeePayerPolicy::default();
1598        policy.system.allow_allocate = false;
1599        setup_config_with_policy(policy);
1600
1601        let config = get_config().unwrap();
1602        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1603        let instruction = allocate(&fee_payer, 100);
1604        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1605        let mut transaction =
1606            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1607        assert!(validator
1608            .validate_transaction(config, &mut transaction, &rpc_client)
1609            .await
1610            .is_err());
1611    }
1612
1613    #[tokio::test]
1614    #[serial]
1615    async fn test_fee_payer_policy_nonce_initialize() {
1616        use solana_system_interface::instruction::create_nonce_account;
1617
1618        let fee_payer = Pubkey::new_unique();
1619        let nonce_account = Pubkey::new_unique();
1620
1621        // Test with allow_initialize = true
1622        let rpc_client = RpcMockBuilder::new().build();
1623        let mut policy = FeePayerPolicy::default();
1624        policy.system.nonce.allow_initialize = true;
1625        setup_config_with_policy(policy);
1626
1627        let config = get_config().unwrap();
1628        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1629        let instructions = create_nonce_account(&fee_payer, &nonce_account, &fee_payer, 1_000_000);
1630        // Only test the InitializeNonceAccount instruction (second one)
1631        let message =
1632            VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer)));
1633        let mut transaction =
1634            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1635        assert!(validator
1636            .validate_transaction(config, &mut transaction, &rpc_client)
1637            .await
1638            .is_ok());
1639
1640        // Test with allow_initialize = false
1641        let rpc_client = RpcMockBuilder::new().build();
1642        let mut policy = FeePayerPolicy::default();
1643        policy.system.nonce.allow_initialize = false;
1644        setup_config_with_policy(policy);
1645
1646        let config = get_config().unwrap();
1647        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1648        let instructions = create_nonce_account(&fee_payer, &nonce_account, &fee_payer, 1_000_000);
1649        let message =
1650            VersionedMessage::Legacy(Message::new(&[instructions[1].clone()], Some(&fee_payer)));
1651        let mut transaction =
1652            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1653        assert!(validator
1654            .validate_transaction(config, &mut transaction, &rpc_client)
1655            .await
1656            .is_err());
1657    }
1658
1659    #[tokio::test]
1660    #[serial]
1661    async fn test_fee_payer_policy_nonce_advance() {
1662        use solana_system_interface::instruction::advance_nonce_account;
1663
1664        let fee_payer = Pubkey::new_unique();
1665        let nonce_account = Pubkey::new_unique();
1666
1667        // Test with allow_advance = true (must also enable durable transactions)
1668        let rpc_client = RpcMockBuilder::new().build();
1669        let mut policy = FeePayerPolicy::default();
1670        policy.system.nonce.allow_advance = true;
1671        let config = ConfigMockBuilder::new()
1672            .with_price_source(PriceSource::Mock)
1673            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
1674            .with_max_allowed_lamports(1_000_000)
1675            .with_fee_payer_policy(policy)
1676            .with_allow_durable_transactions(true)
1677            .build();
1678        update_config(config).unwrap();
1679
1680        let config = get_config().unwrap();
1681        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1682        let instruction = advance_nonce_account(&nonce_account, &fee_payer);
1683        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1684        let mut transaction =
1685            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1686        assert!(validator
1687            .validate_transaction(config, &mut transaction, &rpc_client)
1688            .await
1689            .is_ok());
1690
1691        // Test with allow_advance = false (durable txs enabled but policy blocks it)
1692        let rpc_client = RpcMockBuilder::new().build();
1693        let mut policy = FeePayerPolicy::default();
1694        policy.system.nonce.allow_advance = false;
1695        let config = ConfigMockBuilder::new()
1696            .with_price_source(PriceSource::Mock)
1697            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
1698            .with_max_allowed_lamports(1_000_000)
1699            .with_fee_payer_policy(policy)
1700            .with_allow_durable_transactions(true)
1701            .build();
1702        update_config(config).unwrap();
1703
1704        let config = get_config().unwrap();
1705        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1706        let instruction = advance_nonce_account(&nonce_account, &fee_payer);
1707        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1708        let mut transaction =
1709            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1710        assert!(validator
1711            .validate_transaction(config, &mut transaction, &rpc_client)
1712            .await
1713            .is_err());
1714    }
1715
1716    #[tokio::test]
1717    #[serial]
1718    async fn test_fee_payer_policy_nonce_withdraw() {
1719        use solana_system_interface::instruction::withdraw_nonce_account;
1720
1721        let fee_payer = Pubkey::new_unique();
1722        let nonce_account = Pubkey::new_unique();
1723        let recipient = Pubkey::new_unique();
1724
1725        // Test with allow_withdraw = true
1726        let rpc_client = RpcMockBuilder::new().build();
1727        let mut policy = FeePayerPolicy::default();
1728        policy.system.nonce.allow_withdraw = true;
1729        setup_config_with_policy(policy);
1730
1731        let config = get_config().unwrap();
1732        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1733        let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000);
1734        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1735        let mut transaction =
1736            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1737        assert!(validator
1738            .validate_transaction(config, &mut transaction, &rpc_client)
1739            .await
1740            .is_ok());
1741
1742        // Test with allow_withdraw = false
1743        let rpc_client = RpcMockBuilder::new().build();
1744        let mut policy = FeePayerPolicy::default();
1745        policy.system.nonce.allow_withdraw = false;
1746        setup_config_with_policy(policy);
1747
1748        let config = get_config().unwrap();
1749        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1750        let instruction = withdraw_nonce_account(&nonce_account, &fee_payer, &recipient, 1000);
1751        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1752        let mut transaction =
1753            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1754        assert!(validator
1755            .validate_transaction(config, &mut transaction, &rpc_client)
1756            .await
1757            .is_err());
1758    }
1759
1760    #[tokio::test]
1761    #[serial]
1762    async fn test_fee_payer_policy_nonce_authorize() {
1763        use solana_system_interface::instruction::authorize_nonce_account;
1764
1765        let fee_payer = Pubkey::new_unique();
1766        let nonce_account = Pubkey::new_unique();
1767        let new_authority = Pubkey::new_unique();
1768
1769        // Test with allow_authorize = true
1770        let rpc_client = RpcMockBuilder::new().build();
1771        let mut policy = FeePayerPolicy::default();
1772        policy.system.nonce.allow_authorize = true;
1773        setup_config_with_policy(policy);
1774
1775        let config = get_config().unwrap();
1776        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1777        let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority);
1778        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1779        let mut transaction =
1780            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1781        assert!(validator
1782            .validate_transaction(config, &mut transaction, &rpc_client)
1783            .await
1784            .is_ok());
1785
1786        // Test with allow_authorize = false
1787        let rpc_client = RpcMockBuilder::new().build();
1788        let mut policy = FeePayerPolicy::default();
1789        policy.system.nonce.allow_authorize = false;
1790        setup_config_with_policy(policy);
1791
1792        let config = get_config().unwrap();
1793        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1794        let instruction = authorize_nonce_account(&nonce_account, &fee_payer, &new_authority);
1795        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1796        let mut transaction =
1797            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1798        assert!(validator
1799            .validate_transaction(config, &mut transaction, &rpc_client)
1800            .await
1801            .is_err());
1802    }
1803
1804    #[test]
1805    #[serial]
1806    fn test_strict_pricing_total_exceeds_fixed() {
1807        let mut config = ConfigMockBuilder::new().build();
1808        config.validation.price.model = PriceModel::Fixed {
1809            amount: 5000,
1810            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1811            strict: true,
1812        };
1813        let _ = update_config(config);
1814
1815        // Fixed price = 5000, but total = 3000 + 2000 + 5000 = 10000 > 5000
1816        let fee_calc = TotalFeeCalculation::new(5000, 3000, 2000, 5000, 0, 0);
1817
1818        let config = get_config().unwrap();
1819        let result = TransactionValidator::validate_strict_pricing_with_fee(config, &fee_calc);
1820
1821        assert!(result.is_err());
1822        if let Err(KoraError::ValidationError(msg)) = result {
1823            assert!(msg.contains("Strict pricing violation"));
1824            assert!(msg.contains("exceeds fixed price"));
1825        } else {
1826            panic!("Expected ValidationError");
1827        }
1828    }
1829
1830    #[test]
1831    #[serial]
1832    fn test_strict_pricing_total_within_fixed() {
1833        let mut config = ConfigMockBuilder::new().build();
1834        config.validation.price.model = PriceModel::Fixed {
1835            amount: 5000,
1836            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1837            strict: true,
1838        };
1839        let _ = update_config(config);
1840
1841        // Fixed price = 5000, total = 1000 + 1000 + 1000 = 3000 < 5000
1842        let fee_calc = TotalFeeCalculation::new(5000, 1000, 1000, 1000, 0, 0);
1843
1844        let config = get_config().unwrap();
1845        let result = TransactionValidator::validate_strict_pricing_with_fee(config, &fee_calc);
1846
1847        assert!(result.is_ok());
1848    }
1849
1850    #[test]
1851    #[serial]
1852    fn test_strict_pricing_disabled() {
1853        let mut config = ConfigMockBuilder::new().build();
1854        config.validation.price.model = PriceModel::Fixed {
1855            amount: 5000,
1856            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1857            strict: false, // Disabled
1858        };
1859        let _ = update_config(config);
1860
1861        let fee_calc = TotalFeeCalculation::new(5000, 10000, 0, 0, 0, 0);
1862
1863        let config = get_config().unwrap();
1864        let result = TransactionValidator::validate_strict_pricing_with_fee(config, &fee_calc);
1865
1866        assert!(result.is_ok(), "Should pass when strict=false");
1867    }
1868
1869    #[test]
1870    #[serial]
1871    fn test_strict_pricing_with_margin_pricing() {
1872        use crate::{
1873            fee::price::PriceModel, state::update_config, tests::config_mock::ConfigMockBuilder,
1874        };
1875
1876        let mut config = ConfigMockBuilder::new().build();
1877        config.validation.price.model = PriceModel::Margin { margin: 0.1 };
1878        let _ = update_config(config);
1879
1880        let fee_calc = TotalFeeCalculation::new(5000, 10000, 0, 0, 0, 0);
1881
1882        let config = get_config().unwrap();
1883        let result = TransactionValidator::validate_strict_pricing_with_fee(config, &fee_calc);
1884
1885        assert!(result.is_ok());
1886    }
1887
1888    #[test]
1889    #[serial]
1890    fn test_strict_pricing_exact_match() {
1891        use crate::{
1892            fee::price::PriceModel, state::update_config, tests::config_mock::ConfigMockBuilder,
1893        };
1894
1895        let mut config = ConfigMockBuilder::new().build();
1896        config.validation.price.model = PriceModel::Fixed {
1897            amount: 5000,
1898            token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1899            strict: true,
1900        };
1901        let _ = update_config(config);
1902
1903        // Total exactly equals fixed price (5000 = 5000)
1904        let fee_calc = TotalFeeCalculation::new(5000, 2000, 1000, 2000, 0, 0);
1905
1906        let config = get_config().unwrap();
1907        let result = TransactionValidator::validate_strict_pricing_with_fee(config, &fee_calc);
1908
1909        assert!(result.is_ok(), "Should pass when total equals fixed price");
1910    }
1911
1912    #[tokio::test]
1913    #[serial]
1914    async fn test_durable_transaction_rejected_by_default() {
1915        use solana_system_interface::instruction::advance_nonce_account;
1916
1917        let fee_payer = Pubkey::new_unique();
1918        let nonce_account = Pubkey::new_unique();
1919        let nonce_authority = Pubkey::new_unique(); // Different from fee payer
1920
1921        // Default config has allow_durable_transactions = false
1922        setup_default_config();
1923        let rpc_client = RpcMockBuilder::new().build();
1924
1925        let config = get_config().unwrap();
1926        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1927
1928        // Transaction with AdvanceNonceAccount (authority is NOT fee payer)
1929        let instruction = advance_nonce_account(&nonce_account, &nonce_authority);
1930        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1931        let mut transaction =
1932            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1933
1934        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
1935        assert!(result.is_err());
1936        if let Err(KoraError::InvalidTransaction(msg)) = result {
1937            assert!(msg.contains("Durable transactions"));
1938            assert!(msg.contains("not allowed"));
1939        } else {
1940            panic!("Expected InvalidTransaction error");
1941        }
1942    }
1943
1944    #[tokio::test]
1945    #[serial]
1946    async fn test_durable_transaction_allowed_when_enabled() {
1947        use solana_system_interface::instruction::advance_nonce_account;
1948
1949        let fee_payer = Pubkey::new_unique();
1950        let nonce_account = Pubkey::new_unique();
1951        let nonce_authority = Pubkey::new_unique(); // Different from fee payer
1952
1953        // Enable durable transactions
1954        let mock_config = ConfigMockBuilder::new()
1955            .with_price_source(PriceSource::Mock)
1956            .with_allowed_programs(vec![SYSTEM_PROGRAM_ID.to_string()])
1957            .with_max_allowed_lamports(1_000_000)
1958            .with_fee_payer_policy(FeePayerPolicy::default())
1959            .with_allow_durable_transactions(true)
1960            .build();
1961        update_config(mock_config).unwrap();
1962
1963        let rpc_client = RpcMockBuilder::new().build();
1964        let config = get_config().unwrap();
1965        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1966
1967        // Transaction with AdvanceNonceAccount (authority is NOT fee payer)
1968        let instruction = advance_nonce_account(&nonce_account, &nonce_authority);
1969        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1970        let mut transaction =
1971            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1972
1973        // Should pass because durable transactions are allowed
1974        assert!(validator
1975            .validate_transaction(config, &mut transaction, &rpc_client)
1976            .await
1977            .is_ok());
1978    }
1979
1980    #[tokio::test]
1981    #[serial]
1982    async fn test_non_durable_transaction_passes() {
1983        let fee_payer = Pubkey::new_unique();
1984        let sender = Pubkey::new_unique();
1985        let recipient = Pubkey::new_unique();
1986
1987        // Default config has allow_durable_transactions = false
1988        setup_default_config();
1989        let rpc_client = RpcMockBuilder::new().build();
1990
1991        let config = get_config().unwrap();
1992        let validator = TransactionValidator::new(config, fee_payer).unwrap();
1993
1994        // Regular transfer (no nonce instruction)
1995        let instruction = transfer(&sender, &recipient, 1000);
1996        let message = VersionedMessage::Legacy(Message::new(&[instruction], Some(&fee_payer)));
1997        let mut transaction =
1998            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
1999
2000        // Should pass - no AdvanceNonceAccount instruction
2001        assert!(validator
2002            .validate_transaction(config, &mut transaction, &rpc_client)
2003            .await
2004            .is_ok());
2005    }
2006
2007    #[tokio::test]
2008    #[serial]
2009    async fn test_fee_payer_policy_revoke() {
2010        let fee_payer = Pubkey::new_unique();
2011        let fee_payer_token_account = Pubkey::new_unique();
2012
2013        // Test with allow_revoke = true
2014        let rpc_client = RpcMockBuilder::new().build();
2015        let mut policy = FeePayerPolicy::default();
2016        policy.spl_token.allow_revoke = true;
2017        setup_spl_config_with_policy(policy);
2018
2019        let config = get_config().unwrap();
2020        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2021
2022        let revoke_ix = spl_token_interface::instruction::revoke(
2023            &spl_token_interface::id(),
2024            &fee_payer_token_account,
2025            &fee_payer,
2026            &[],
2027        )
2028        .unwrap();
2029
2030        let message = VersionedMessage::Legacy(Message::new(&[revoke_ix], Some(&fee_payer)));
2031        let mut transaction =
2032            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2033        assert!(validator
2034            .validate_transaction(config, &mut transaction, &rpc_client)
2035            .await
2036            .is_ok());
2037
2038        // Test with allow_revoke = false
2039        let rpc_client = RpcMockBuilder::new().build();
2040        let mut policy = FeePayerPolicy::default();
2041        policy.spl_token.allow_revoke = false;
2042        setup_spl_config_with_policy(policy);
2043
2044        let config = get_config().unwrap();
2045        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2046
2047        let revoke_ix = spl_token_interface::instruction::revoke(
2048            &spl_token_interface::id(),
2049            &fee_payer_token_account,
2050            &fee_payer,
2051            &[],
2052        )
2053        .unwrap();
2054
2055        let message = VersionedMessage::Legacy(Message::new(&[revoke_ix], Some(&fee_payer)));
2056        let mut transaction =
2057            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2058        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2059        if let Err(KoraError::InvalidTransaction(msg)) = result {
2060            assert!(msg.contains("Fee payer cannot be used for"));
2061        } else {
2062            panic!("Expected InvalidTransaction error for revoke policy");
2063        }
2064    }
2065
2066    #[tokio::test]
2067    #[serial]
2068    async fn test_fee_payer_policy_token2022_revoke() {
2069        let fee_payer = Pubkey::new_unique();
2070        let fee_payer_token_account = Pubkey::new_unique();
2071
2072        // Test with allow_revoke = true
2073        let rpc_client = RpcMockBuilder::new().build();
2074        let mut policy = FeePayerPolicy::default();
2075        policy.token_2022.allow_revoke = true;
2076        setup_token2022_config_with_policy(policy);
2077
2078        let config = get_config().unwrap();
2079        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2080
2081        let revoke_ix = spl_token_2022_interface::instruction::revoke(
2082            &spl_token_2022_interface::id(),
2083            &fee_payer_token_account,
2084            &fee_payer,
2085            &[],
2086        )
2087        .unwrap();
2088
2089        let message = VersionedMessage::Legacy(Message::new(&[revoke_ix], Some(&fee_payer)));
2090        let mut transaction =
2091            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2092        assert!(validator
2093            .validate_transaction(config, &mut transaction, &rpc_client)
2094            .await
2095            .is_ok());
2096
2097        // Test with allow_revoke = false
2098        let rpc_client = RpcMockBuilder::new().build();
2099        let mut policy = FeePayerPolicy::default();
2100        policy.token_2022.allow_revoke = false;
2101        setup_token2022_config_with_policy(policy);
2102
2103        let config = get_config().unwrap();
2104        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2105
2106        let revoke_ix = spl_token_2022_interface::instruction::revoke(
2107            &spl_token_2022_interface::id(),
2108            &fee_payer_token_account,
2109            &fee_payer,
2110            &[],
2111        )
2112        .unwrap();
2113
2114        let message = VersionedMessage::Legacy(Message::new(&[revoke_ix], Some(&fee_payer)));
2115        let mut transaction =
2116            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2117        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2118        if let Err(KoraError::InvalidTransaction(msg)) = result {
2119            assert!(msg.contains("Fee payer cannot be used for"));
2120        } else {
2121            panic!("Expected InvalidTransaction error for token2022_revoke policy");
2122        }
2123    }
2124
2125    #[tokio::test]
2126    #[serial]
2127    async fn test_fee_payer_policy_set_authority() {
2128        let fee_payer = Pubkey::new_unique();
2129        let fee_payer_token_account = Pubkey::new_unique();
2130        let new_authority = Pubkey::new_unique();
2131
2132        // Test with allow_set_authority = true
2133        let rpc_client = RpcMockBuilder::new().build();
2134        let mut policy = FeePayerPolicy::default();
2135        policy.spl_token.allow_set_authority = true;
2136        setup_spl_config_with_policy(policy);
2137
2138        let config = get_config().unwrap();
2139        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2140
2141        let set_authority_ix = spl_token_interface::instruction::set_authority(
2142            &spl_token_interface::id(),
2143            &fee_payer_token_account,
2144            Some(&new_authority),
2145            spl_token_interface::instruction::AuthorityType::AccountOwner,
2146            &fee_payer,
2147            &[],
2148        )
2149        .unwrap();
2150
2151        let message = VersionedMessage::Legacy(Message::new(&[set_authority_ix], Some(&fee_payer)));
2152        let mut transaction =
2153            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2154        assert!(validator
2155            .validate_transaction(config, &mut transaction, &rpc_client)
2156            .await
2157            .is_ok());
2158
2159        // Test with allow_set_authority = false
2160        let rpc_client = RpcMockBuilder::new().build();
2161        let mut policy = FeePayerPolicy::default();
2162        policy.spl_token.allow_set_authority = false;
2163        setup_spl_config_with_policy(policy);
2164
2165        let config = get_config().unwrap();
2166        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2167
2168        let set_authority_ix = spl_token_interface::instruction::set_authority(
2169            &spl_token_interface::id(),
2170            &fee_payer_token_account,
2171            Some(&new_authority),
2172            spl_token_interface::instruction::AuthorityType::AccountOwner,
2173            &fee_payer,
2174            &[],
2175        )
2176        .unwrap();
2177
2178        let message = VersionedMessage::Legacy(Message::new(&[set_authority_ix], Some(&fee_payer)));
2179        let mut transaction =
2180            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2181        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2182        if let Err(KoraError::InvalidTransaction(msg)) = result {
2183            assert!(msg.contains("Fee payer cannot be used for"));
2184        } else {
2185            panic!("Expected InvalidTransaction error for set_authority policy");
2186        }
2187    }
2188
2189    #[tokio::test]
2190    #[serial]
2191    async fn test_fee_payer_policy_token2022_set_authority() {
2192        let fee_payer = Pubkey::new_unique();
2193        let fee_payer_token_account = Pubkey::new_unique();
2194        let new_authority = Pubkey::new_unique();
2195
2196        // Test with allow_set_authority = true
2197        let rpc_client = RpcMockBuilder::new().build();
2198        let mut policy = FeePayerPolicy::default();
2199        policy.token_2022.allow_set_authority = true;
2200        setup_token2022_config_with_policy(policy);
2201
2202        let config = get_config().unwrap();
2203        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2204
2205        let set_authority_ix = spl_token_2022_interface::instruction::set_authority(
2206            &spl_token_2022_interface::id(),
2207            &fee_payer_token_account,
2208            Some(&new_authority),
2209            spl_token_2022_interface::instruction::AuthorityType::AccountOwner,
2210            &fee_payer,
2211            &[],
2212        )
2213        .unwrap();
2214
2215        let message = VersionedMessage::Legacy(Message::new(&[set_authority_ix], Some(&fee_payer)));
2216        let mut transaction =
2217            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2218        assert!(validator
2219            .validate_transaction(config, &mut transaction, &rpc_client)
2220            .await
2221            .is_ok());
2222
2223        // Test with allow_set_authority = false
2224        let rpc_client = RpcMockBuilder::new().build();
2225        let mut policy = FeePayerPolicy::default();
2226        policy.token_2022.allow_set_authority = false;
2227        setup_token2022_config_with_policy(policy);
2228
2229        let config = get_config().unwrap();
2230        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2231
2232        let set_authority_ix = spl_token_2022_interface::instruction::set_authority(
2233            &spl_token_2022_interface::id(),
2234            &fee_payer_token_account,
2235            Some(&new_authority),
2236            spl_token_2022_interface::instruction::AuthorityType::AccountOwner,
2237            &fee_payer,
2238            &[],
2239        )
2240        .unwrap();
2241
2242        let message = VersionedMessage::Legacy(Message::new(&[set_authority_ix], Some(&fee_payer)));
2243        let mut transaction =
2244            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2245        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2246        if let Err(KoraError::InvalidTransaction(msg)) = result {
2247            assert!(msg.contains("Fee payer cannot be used for"));
2248        } else {
2249            panic!("Expected InvalidTransaction error for token2022_set_authority policy");
2250        }
2251    }
2252
2253    #[tokio::test]
2254    #[serial]
2255    async fn test_fee_payer_policy_mint_to() {
2256        let fee_payer = Pubkey::new_unique();
2257        let mint = Pubkey::new_unique();
2258        let destination_token_account = Pubkey::new_unique();
2259
2260        // Test with allow_mint_to = true
2261        let rpc_client = RpcMockBuilder::new().build();
2262        let mut policy = FeePayerPolicy::default();
2263        policy.spl_token.allow_mint_to = true;
2264        setup_spl_config_with_policy(policy);
2265
2266        let config = get_config().unwrap();
2267        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2268
2269        let mint_to_ix = spl_token_interface::instruction::mint_to(
2270            &spl_token_interface::id(),
2271            &mint,
2272            &destination_token_account,
2273            &fee_payer,
2274            &[],
2275            1000,
2276        )
2277        .unwrap();
2278
2279        let message = VersionedMessage::Legacy(Message::new(&[mint_to_ix], Some(&fee_payer)));
2280        let mut transaction =
2281            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2282        assert!(validator
2283            .validate_transaction(config, &mut transaction, &rpc_client)
2284            .await
2285            .is_ok());
2286
2287        // Test with allow_mint_to = false
2288        let rpc_client = RpcMockBuilder::new().build();
2289        let mut policy = FeePayerPolicy::default();
2290        policy.spl_token.allow_mint_to = false;
2291        setup_spl_config_with_policy(policy);
2292
2293        let config = get_config().unwrap();
2294        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2295
2296        let mint_to_ix = spl_token_interface::instruction::mint_to(
2297            &spl_token_interface::id(),
2298            &mint,
2299            &destination_token_account,
2300            &fee_payer,
2301            &[],
2302            1000,
2303        )
2304        .unwrap();
2305
2306        let message = VersionedMessage::Legacy(Message::new(&[mint_to_ix], Some(&fee_payer)));
2307        let mut transaction =
2308            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2309        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2310        if let Err(KoraError::InvalidTransaction(msg)) = result {
2311            assert!(msg.contains("Fee payer cannot be used for"));
2312        } else {
2313            panic!("Expected InvalidTransaction error for mint_to policy");
2314        }
2315    }
2316
2317    #[tokio::test]
2318    #[serial]
2319    async fn test_fee_payer_policy_token2022_mint_to() {
2320        let fee_payer = Pubkey::new_unique();
2321        let mint = Pubkey::new_unique();
2322        let destination_token_account = Pubkey::new_unique();
2323
2324        // Test with allow_mint_to = true
2325        let rpc_client = RpcMockBuilder::new().build();
2326        let mut policy = FeePayerPolicy::default();
2327        policy.token_2022.allow_mint_to = true;
2328        setup_token2022_config_with_policy(policy);
2329
2330        let config = get_config().unwrap();
2331        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2332
2333        let mint_to_ix = spl_token_2022_interface::instruction::mint_to(
2334            &spl_token_2022_interface::id(),
2335            &mint,
2336            &destination_token_account,
2337            &fee_payer,
2338            &[],
2339            1000,
2340        )
2341        .unwrap();
2342
2343        let message = VersionedMessage::Legacy(Message::new(&[mint_to_ix], Some(&fee_payer)));
2344        let mut transaction =
2345            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2346        assert!(validator
2347            .validate_transaction(config, &mut transaction, &rpc_client)
2348            .await
2349            .is_ok());
2350
2351        // Test with allow_mint_to = false
2352        let rpc_client = RpcMockBuilder::new().build();
2353        let mut policy = FeePayerPolicy::default();
2354        policy.token_2022.allow_mint_to = false;
2355        setup_token2022_config_with_policy(policy);
2356
2357        let config = get_config().unwrap();
2358        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2359
2360        let mint_to_ix = spl_token_2022_interface::instruction::mint_to(
2361            &spl_token_2022_interface::id(),
2362            &mint,
2363            &destination_token_account,
2364            &fee_payer,
2365            &[],
2366            1000,
2367        )
2368        .unwrap();
2369
2370        let message = VersionedMessage::Legacy(Message::new(&[mint_to_ix], Some(&fee_payer)));
2371        let mut transaction =
2372            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2373        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2374        if let Err(KoraError::InvalidTransaction(msg)) = result {
2375            assert!(msg.contains("Fee payer cannot be used for"));
2376        } else {
2377            panic!("Expected InvalidTransaction error for token2022_mint_to policy");
2378        }
2379    }
2380
2381    #[tokio::test]
2382    #[serial]
2383    async fn test_fee_payer_policy_initialize_mint() {
2384        let fee_payer = Pubkey::new_unique();
2385        let mint_account = Pubkey::new_unique();
2386
2387        // Test with allow_initialize_mint = true
2388        let rpc_client = RpcMockBuilder::new().build();
2389        let mut policy = FeePayerPolicy::default();
2390        policy.spl_token.allow_initialize_mint = true;
2391        setup_spl_config_with_policy(policy);
2392
2393        let config = get_config().unwrap();
2394        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2395
2396        // fee_payer is the mint_authority (encoded in instruction data)
2397        let init_mint_ix = spl_token_interface::instruction::initialize_mint(
2398            &spl_token_interface::id(),
2399            &mint_account,
2400            &fee_payer,
2401            None,
2402            6,
2403        )
2404        .unwrap();
2405
2406        let message = VersionedMessage::Legacy(Message::new(&[init_mint_ix], Some(&fee_payer)));
2407        let mut transaction =
2408            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2409        assert!(validator
2410            .validate_transaction(config, &mut transaction, &rpc_client)
2411            .await
2412            .is_ok());
2413
2414        // Test with allow_initialize_mint = false
2415        let rpc_client = RpcMockBuilder::new().build();
2416        let mut policy = FeePayerPolicy::default();
2417        policy.spl_token.allow_initialize_mint = false;
2418        setup_spl_config_with_policy(policy);
2419
2420        let config = get_config().unwrap();
2421        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2422
2423        let init_mint_ix = spl_token_interface::instruction::initialize_mint(
2424            &spl_token_interface::id(),
2425            &mint_account,
2426            &fee_payer,
2427            None,
2428            6,
2429        )
2430        .unwrap();
2431
2432        let message = VersionedMessage::Legacy(Message::new(&[init_mint_ix], Some(&fee_payer)));
2433        let mut transaction =
2434            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2435        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2436        if let Err(KoraError::InvalidTransaction(msg)) = result {
2437            assert!(msg.contains("Fee payer cannot be used for"));
2438        } else {
2439            panic!("Expected InvalidTransaction error for initialize_mint policy");
2440        }
2441    }
2442
2443    #[tokio::test]
2444    #[serial]
2445    async fn test_fee_payer_policy_token2022_initialize_mint() {
2446        let fee_payer = Pubkey::new_unique();
2447        let mint_account = Pubkey::new_unique();
2448
2449        // Test with allow_initialize_mint = true
2450        let rpc_client = RpcMockBuilder::new().build();
2451        let mut policy = FeePayerPolicy::default();
2452        policy.token_2022.allow_initialize_mint = true;
2453        setup_token2022_config_with_policy(policy);
2454
2455        let config = get_config().unwrap();
2456        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2457
2458        let init_mint_ix = spl_token_2022_interface::instruction::initialize_mint(
2459            &spl_token_2022_interface::id(),
2460            &mint_account,
2461            &fee_payer,
2462            None,
2463            6,
2464        )
2465        .unwrap();
2466
2467        let message = VersionedMessage::Legacy(Message::new(&[init_mint_ix], Some(&fee_payer)));
2468        let mut transaction =
2469            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2470        assert!(validator
2471            .validate_transaction(config, &mut transaction, &rpc_client)
2472            .await
2473            .is_ok());
2474
2475        // Test with allow_initialize_mint = false
2476        let rpc_client = RpcMockBuilder::new().build();
2477        let mut policy = FeePayerPolicy::default();
2478        policy.token_2022.allow_initialize_mint = false;
2479        setup_token2022_config_with_policy(policy);
2480
2481        let config = get_config().unwrap();
2482        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2483
2484        let init_mint_ix = spl_token_2022_interface::instruction::initialize_mint(
2485            &spl_token_2022_interface::id(),
2486            &mint_account,
2487            &fee_payer,
2488            None,
2489            6,
2490        )
2491        .unwrap();
2492
2493        let message = VersionedMessage::Legacy(Message::new(&[init_mint_ix], Some(&fee_payer)));
2494        let mut transaction =
2495            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2496        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2497        if let Err(KoraError::InvalidTransaction(msg)) = result {
2498            assert!(msg.contains("Fee payer cannot be used for"));
2499        } else {
2500            panic!("Expected InvalidTransaction error for token2022_initialize_mint policy");
2501        }
2502    }
2503
2504    #[tokio::test]
2505    #[serial]
2506    async fn test_fee_payer_policy_initialize_account() {
2507        let fee_payer = Pubkey::new_unique();
2508        let token_account = Pubkey::new_unique();
2509        let mint = Pubkey::new_unique();
2510
2511        // Test with allow_initialize_account = true
2512        // initialize_account puts owner at account index 2 (token_account, mint, owner, rent_sysvar)
2513        let rpc_client = RpcMockBuilder::new().build();
2514        let mut policy = FeePayerPolicy::default();
2515        policy.spl_token.allow_initialize_account = true;
2516        setup_spl_config_with_policy(policy);
2517
2518        let config = get_config().unwrap();
2519        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2520
2521        let init_account_ix = spl_token_interface::instruction::initialize_account(
2522            &spl_token_interface::id(),
2523            &token_account,
2524            &mint,
2525            &fee_payer,
2526        )
2527        .unwrap();
2528
2529        let message = VersionedMessage::Legacy(Message::new(&[init_account_ix], Some(&fee_payer)));
2530        let mut transaction =
2531            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2532        assert!(validator
2533            .validate_transaction(config, &mut transaction, &rpc_client)
2534            .await
2535            .is_ok());
2536
2537        // Test with allow_initialize_account = false
2538        let rpc_client = RpcMockBuilder::new().build();
2539        let mut policy = FeePayerPolicy::default();
2540        policy.spl_token.allow_initialize_account = false;
2541        setup_spl_config_with_policy(policy);
2542
2543        let config = get_config().unwrap();
2544        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2545
2546        let init_account_ix = spl_token_interface::instruction::initialize_account(
2547            &spl_token_interface::id(),
2548            &token_account,
2549            &mint,
2550            &fee_payer,
2551        )
2552        .unwrap();
2553
2554        let message = VersionedMessage::Legacy(Message::new(&[init_account_ix], Some(&fee_payer)));
2555        let mut transaction =
2556            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2557        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2558        if let Err(KoraError::InvalidTransaction(msg)) = result {
2559            assert!(msg.contains("Fee payer cannot be used for"));
2560        } else {
2561            panic!("Expected InvalidTransaction error for initialize_account policy");
2562        }
2563    }
2564
2565    #[tokio::test]
2566    #[serial]
2567    async fn test_fee_payer_policy_token2022_initialize_account() {
2568        let fee_payer = Pubkey::new_unique();
2569        let token_account = Pubkey::new_unique();
2570        let mint = Pubkey::new_unique();
2571
2572        // Test with allow_initialize_account = true
2573        let rpc_client = RpcMockBuilder::new().build();
2574        let mut policy = FeePayerPolicy::default();
2575        policy.token_2022.allow_initialize_account = true;
2576        setup_token2022_config_with_policy(policy);
2577
2578        let config = get_config().unwrap();
2579        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2580
2581        let init_account_ix = spl_token_2022_interface::instruction::initialize_account(
2582            &spl_token_2022_interface::id(),
2583            &token_account,
2584            &mint,
2585            &fee_payer,
2586        )
2587        .unwrap();
2588
2589        let message = VersionedMessage::Legacy(Message::new(&[init_account_ix], Some(&fee_payer)));
2590        let mut transaction =
2591            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2592        assert!(validator
2593            .validate_transaction(config, &mut transaction, &rpc_client)
2594            .await
2595            .is_ok());
2596
2597        // Test with allow_initialize_account = false
2598        let rpc_client = RpcMockBuilder::new().build();
2599        let mut policy = FeePayerPolicy::default();
2600        policy.token_2022.allow_initialize_account = false;
2601        setup_token2022_config_with_policy(policy);
2602
2603        let config = get_config().unwrap();
2604        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2605
2606        let init_account_ix = spl_token_2022_interface::instruction::initialize_account(
2607            &spl_token_2022_interface::id(),
2608            &token_account,
2609            &mint,
2610            &fee_payer,
2611        )
2612        .unwrap();
2613
2614        let message = VersionedMessage::Legacy(Message::new(&[init_account_ix], Some(&fee_payer)));
2615        let mut transaction =
2616            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2617        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2618        if let Err(KoraError::InvalidTransaction(msg)) = result {
2619            assert!(msg.contains("Fee payer cannot be used for"));
2620        } else {
2621            panic!("Expected InvalidTransaction error for token2022_initialize_account policy");
2622        }
2623    }
2624
2625    #[tokio::test]
2626    #[serial]
2627    async fn test_fee_payer_policy_initialize_multisig() {
2628        let fee_payer = Pubkey::new_unique();
2629        let multisig_account = Pubkey::new_unique();
2630        let other_signer = Pubkey::new_unique();
2631
2632        // Test with allow_initialize_multisig = true
2633        // fee_payer is one of the signers (parsed from accounts[2..])
2634        let rpc_client = RpcMockBuilder::new().build();
2635        let mut policy = FeePayerPolicy::default();
2636        policy.spl_token.allow_initialize_multisig = true;
2637        setup_spl_config_with_policy(policy);
2638
2639        let config = get_config().unwrap();
2640        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2641
2642        let init_multisig_ix = spl_token_interface::instruction::initialize_multisig(
2643            &spl_token_interface::id(),
2644            &multisig_account,
2645            &[&fee_payer, &other_signer],
2646            2,
2647        )
2648        .unwrap();
2649
2650        let message = VersionedMessage::Legacy(Message::new(&[init_multisig_ix], Some(&fee_payer)));
2651        let mut transaction =
2652            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2653        assert!(validator
2654            .validate_transaction(config, &mut transaction, &rpc_client)
2655            .await
2656            .is_ok());
2657
2658        // Test with allow_initialize_multisig = false
2659        let rpc_client = RpcMockBuilder::new().build();
2660        let mut policy = FeePayerPolicy::default();
2661        policy.spl_token.allow_initialize_multisig = false;
2662        setup_spl_config_with_policy(policy);
2663
2664        let config = get_config().unwrap();
2665        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2666
2667        let init_multisig_ix = spl_token_interface::instruction::initialize_multisig(
2668            &spl_token_interface::id(),
2669            &multisig_account,
2670            &[&fee_payer, &other_signer],
2671            2,
2672        )
2673        .unwrap();
2674
2675        let message = VersionedMessage::Legacy(Message::new(&[init_multisig_ix], Some(&fee_payer)));
2676        let mut transaction =
2677            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2678        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2679        if let Err(KoraError::InvalidTransaction(msg)) = result {
2680            assert!(msg.contains("Fee payer cannot be used for"));
2681        } else {
2682            panic!("Expected InvalidTransaction error for initialize_multisig policy");
2683        }
2684    }
2685
2686    #[tokio::test]
2687    #[serial]
2688    async fn test_fee_payer_policy_token2022_initialize_multisig() {
2689        let fee_payer = Pubkey::new_unique();
2690        let multisig_account = Pubkey::new_unique();
2691        let other_signer = Pubkey::new_unique();
2692
2693        // Test with allow_initialize_multisig = true
2694        let rpc_client = RpcMockBuilder::new().build();
2695        let mut policy = FeePayerPolicy::default();
2696        policy.token_2022.allow_initialize_multisig = true;
2697        setup_token2022_config_with_policy(policy);
2698
2699        let config = get_config().unwrap();
2700        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2701
2702        let init_multisig_ix = spl_token_2022_interface::instruction::initialize_multisig(
2703            &spl_token_2022_interface::id(),
2704            &multisig_account,
2705            &[&fee_payer, &other_signer],
2706            2,
2707        )
2708        .unwrap();
2709
2710        let message = VersionedMessage::Legacy(Message::new(&[init_multisig_ix], Some(&fee_payer)));
2711        let mut transaction =
2712            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2713        assert!(validator
2714            .validate_transaction(config, &mut transaction, &rpc_client)
2715            .await
2716            .is_ok());
2717
2718        // Test with allow_initialize_multisig = false
2719        let rpc_client = RpcMockBuilder::new().build();
2720        let mut policy = FeePayerPolicy::default();
2721        policy.token_2022.allow_initialize_multisig = false;
2722        setup_token2022_config_with_policy(policy);
2723
2724        let config = get_config().unwrap();
2725        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2726
2727        let init_multisig_ix = spl_token_2022_interface::instruction::initialize_multisig(
2728            &spl_token_2022_interface::id(),
2729            &multisig_account,
2730            &[&fee_payer, &other_signer],
2731            2,
2732        )
2733        .unwrap();
2734
2735        let message = VersionedMessage::Legacy(Message::new(&[init_multisig_ix], Some(&fee_payer)));
2736        let mut transaction =
2737            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2738        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2739        if let Err(KoraError::InvalidTransaction(msg)) = result {
2740            assert!(msg.contains("Fee payer cannot be used for"));
2741        } else {
2742            panic!("Expected InvalidTransaction error for token2022_initialize_multisig policy");
2743        }
2744    }
2745
2746    #[tokio::test]
2747    #[serial]
2748    async fn test_fee_payer_policy_freeze_account() {
2749        let fee_payer = Pubkey::new_unique();
2750        let token_account = Pubkey::new_unique();
2751        let mint = Pubkey::new_unique();
2752
2753        // Test with allow_freeze_account = true
2754        // freeze_account(program_id, account, mint, freeze_authority, signers) — freeze_authority at index 2
2755        let rpc_client = RpcMockBuilder::new().build();
2756        let mut policy = FeePayerPolicy::default();
2757        policy.spl_token.allow_freeze_account = true;
2758        setup_spl_config_with_policy(policy);
2759
2760        let config = get_config().unwrap();
2761        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2762
2763        let freeze_ix = spl_token_interface::instruction::freeze_account(
2764            &spl_token_interface::id(),
2765            &token_account,
2766            &mint,
2767            &fee_payer,
2768            &[],
2769        )
2770        .unwrap();
2771
2772        let message = VersionedMessage::Legacy(Message::new(&[freeze_ix], Some(&fee_payer)));
2773        let mut transaction =
2774            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2775        assert!(validator
2776            .validate_transaction(config, &mut transaction, &rpc_client)
2777            .await
2778            .is_ok());
2779
2780        // Test with allow_freeze_account = false
2781        let rpc_client = RpcMockBuilder::new().build();
2782        let mut policy = FeePayerPolicy::default();
2783        policy.spl_token.allow_freeze_account = false;
2784        setup_spl_config_with_policy(policy);
2785
2786        let config = get_config().unwrap();
2787        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2788
2789        let freeze_ix = spl_token_interface::instruction::freeze_account(
2790            &spl_token_interface::id(),
2791            &token_account,
2792            &mint,
2793            &fee_payer,
2794            &[],
2795        )
2796        .unwrap();
2797
2798        let message = VersionedMessage::Legacy(Message::new(&[freeze_ix], Some(&fee_payer)));
2799        let mut transaction =
2800            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2801        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2802        if let Err(KoraError::InvalidTransaction(msg)) = result {
2803            assert!(msg.contains("Fee payer cannot be used for"));
2804        } else {
2805            panic!("Expected InvalidTransaction error for freeze_account policy");
2806        }
2807    }
2808
2809    #[tokio::test]
2810    #[serial]
2811    async fn test_fee_payer_policy_token2022_freeze_account() {
2812        let fee_payer = Pubkey::new_unique();
2813        let token_account = Pubkey::new_unique();
2814        let mint = Pubkey::new_unique();
2815
2816        // Test with allow_freeze_account = true
2817        let rpc_client = RpcMockBuilder::new().build();
2818        let mut policy = FeePayerPolicy::default();
2819        policy.token_2022.allow_freeze_account = true;
2820        setup_token2022_config_with_policy(policy);
2821
2822        let config = get_config().unwrap();
2823        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2824
2825        let freeze_ix = spl_token_2022_interface::instruction::freeze_account(
2826            &spl_token_2022_interface::id(),
2827            &token_account,
2828            &mint,
2829            &fee_payer,
2830            &[],
2831        )
2832        .unwrap();
2833
2834        let message = VersionedMessage::Legacy(Message::new(&[freeze_ix], Some(&fee_payer)));
2835        let mut transaction =
2836            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2837        assert!(validator
2838            .validate_transaction(config, &mut transaction, &rpc_client)
2839            .await
2840            .is_ok());
2841
2842        // Test with allow_freeze_account = false
2843        let rpc_client = RpcMockBuilder::new().build();
2844        let mut policy = FeePayerPolicy::default();
2845        policy.token_2022.allow_freeze_account = false;
2846        setup_token2022_config_with_policy(policy);
2847
2848        let config = get_config().unwrap();
2849        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2850
2851        let freeze_ix = spl_token_2022_interface::instruction::freeze_account(
2852            &spl_token_2022_interface::id(),
2853            &token_account,
2854            &mint,
2855            &fee_payer,
2856            &[],
2857        )
2858        .unwrap();
2859
2860        let message = VersionedMessage::Legacy(Message::new(&[freeze_ix], Some(&fee_payer)));
2861        let mut transaction =
2862            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2863        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2864        if let Err(KoraError::InvalidTransaction(msg)) = result {
2865            assert!(msg.contains("Fee payer cannot be used for"));
2866        } else {
2867            panic!("Expected InvalidTransaction error for token2022_freeze_account policy");
2868        }
2869    }
2870
2871    #[tokio::test]
2872    #[serial]
2873    async fn test_fee_payer_policy_thaw_account() {
2874        let fee_payer = Pubkey::new_unique();
2875        let token_account = Pubkey::new_unique();
2876        let mint = Pubkey::new_unique();
2877
2878        // Test with allow_thaw_account = true
2879        // thaw_account(program_id, account, mint, freeze_authority, signers) — freeze_authority at index 2
2880        let rpc_client = RpcMockBuilder::new().build();
2881        let mut policy = FeePayerPolicy::default();
2882        policy.spl_token.allow_thaw_account = true;
2883        setup_spl_config_with_policy(policy);
2884
2885        let config = get_config().unwrap();
2886        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2887
2888        let thaw_ix = spl_token_interface::instruction::thaw_account(
2889            &spl_token_interface::id(),
2890            &token_account,
2891            &mint,
2892            &fee_payer,
2893            &[],
2894        )
2895        .unwrap();
2896
2897        let message = VersionedMessage::Legacy(Message::new(&[thaw_ix], Some(&fee_payer)));
2898        let mut transaction =
2899            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2900        assert!(validator
2901            .validate_transaction(config, &mut transaction, &rpc_client)
2902            .await
2903            .is_ok());
2904
2905        // Test with allow_thaw_account = false
2906        let rpc_client = RpcMockBuilder::new().build();
2907        let mut policy = FeePayerPolicy::default();
2908        policy.spl_token.allow_thaw_account = false;
2909        setup_spl_config_with_policy(policy);
2910
2911        let config = get_config().unwrap();
2912        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2913
2914        let thaw_ix = spl_token_interface::instruction::thaw_account(
2915            &spl_token_interface::id(),
2916            &token_account,
2917            &mint,
2918            &fee_payer,
2919            &[],
2920        )
2921        .unwrap();
2922
2923        let message = VersionedMessage::Legacy(Message::new(&[thaw_ix], Some(&fee_payer)));
2924        let mut transaction =
2925            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2926        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2927        if let Err(KoraError::InvalidTransaction(msg)) = result {
2928            assert!(msg.contains("Fee payer cannot be used for"));
2929        } else {
2930            panic!("Expected InvalidTransaction error for thaw_account policy");
2931        }
2932    }
2933
2934    #[tokio::test]
2935    #[serial]
2936    async fn test_fee_payer_policy_token2022_thaw_account() {
2937        let fee_payer = Pubkey::new_unique();
2938        let token_account = Pubkey::new_unique();
2939        let mint = Pubkey::new_unique();
2940
2941        // Test with allow_thaw_account = true
2942        let rpc_client = RpcMockBuilder::new().build();
2943        let mut policy = FeePayerPolicy::default();
2944        policy.token_2022.allow_thaw_account = true;
2945        setup_token2022_config_with_policy(policy);
2946
2947        let config = get_config().unwrap();
2948        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2949
2950        let thaw_ix = spl_token_2022_interface::instruction::thaw_account(
2951            &spl_token_2022_interface::id(),
2952            &token_account,
2953            &mint,
2954            &fee_payer,
2955            &[],
2956        )
2957        .unwrap();
2958
2959        let message = VersionedMessage::Legacy(Message::new(&[thaw_ix], Some(&fee_payer)));
2960        let mut transaction =
2961            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2962        assert!(validator
2963            .validate_transaction(config, &mut transaction, &rpc_client)
2964            .await
2965            .is_ok());
2966
2967        // Test with allow_thaw_account = false
2968        let rpc_client = RpcMockBuilder::new().build();
2969        let mut policy = FeePayerPolicy::default();
2970        policy.token_2022.allow_thaw_account = false;
2971        setup_token2022_config_with_policy(policy);
2972
2973        let config = get_config().unwrap();
2974        let validator = TransactionValidator::new(config, fee_payer).unwrap();
2975
2976        let thaw_ix = spl_token_2022_interface::instruction::thaw_account(
2977            &spl_token_2022_interface::id(),
2978            &token_account,
2979            &mint,
2980            &fee_payer,
2981            &[],
2982        )
2983        .unwrap();
2984
2985        let message = VersionedMessage::Legacy(Message::new(&[thaw_ix], Some(&fee_payer)));
2986        let mut transaction =
2987            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
2988        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
2989        if let Err(KoraError::InvalidTransaction(msg)) = result {
2990            assert!(msg.contains("Fee payer cannot be used for"));
2991        } else {
2992            panic!("Expected InvalidTransaction error for token2022_thaw_account policy");
2993        }
2994    }
2995
2996    #[tokio::test]
2997    #[serial]
2998    async fn test_fee_payer_policy_mixed_instructions() {
2999        let fee_payer = Pubkey::new_unique();
3000        let fee_payer_token_account = Pubkey::new_unique();
3001        let mint = Pubkey::new_unique();
3002
3003        let revoke_ix = spl_token_interface::instruction::revoke(
3004            &spl_token_interface::id(),
3005            &fee_payer_token_account,
3006            &fee_payer,
3007            &[],
3008        )
3009        .unwrap();
3010
3011        let burn_ix = spl_token_interface::instruction::burn(
3012            &spl_token_interface::id(),
3013            &fee_payer_token_account,
3014            &mint,
3015            &fee_payer,
3016            &[],
3017            500,
3018        )
3019        .unwrap();
3020
3021        // --- Test 1: revoke=true, burn=true → is_ok() ---
3022        let rpc_client = RpcMockBuilder::new().build();
3023        let mut policy = FeePayerPolicy::default();
3024        policy.spl_token.allow_revoke = true;
3025        policy.spl_token.allow_burn = true;
3026        setup_spl_config_with_policy(policy);
3027
3028        let config = get_config().unwrap();
3029        let validator = TransactionValidator::new(config, fee_payer).unwrap();
3030
3031        let message = VersionedMessage::Legacy(Message::new(
3032            &[revoke_ix.clone(), burn_ix.clone()],
3033            Some(&fee_payer),
3034        ));
3035        let mut transaction =
3036            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
3037        assert!(
3038            validator.validate_transaction(config, &mut transaction, &rpc_client).await.is_ok(),
3039            "Both policies true should pass"
3040        );
3041
3042        // --- Test 2: revoke=true, burn=false → is_err() ---
3043        let rpc_client = RpcMockBuilder::new().build();
3044        let mut policy = FeePayerPolicy::default();
3045        policy.spl_token.allow_revoke = true;
3046        policy.spl_token.allow_burn = false;
3047        setup_spl_config_with_policy(policy);
3048
3049        let config = get_config().unwrap();
3050        let validator = TransactionValidator::new(config, fee_payer).unwrap();
3051
3052        let message = VersionedMessage::Legacy(Message::new(
3053            &[revoke_ix.clone(), burn_ix.clone()],
3054            Some(&fee_payer),
3055        ));
3056        let mut transaction =
3057            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
3058        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
3059        if let Err(KoraError::InvalidTransaction(msg)) = result {
3060            assert!(msg.contains("Fee payer cannot be used for"));
3061        } else {
3062            panic!("Expected InvalidTransaction error for burn policy");
3063        }
3064
3065        // --- Test 3: revoke=false, burn=true → is_err() ---
3066        let rpc_client = RpcMockBuilder::new().build();
3067        let mut policy = FeePayerPolicy::default();
3068        policy.spl_token.allow_revoke = false;
3069        policy.spl_token.allow_burn = true;
3070        setup_spl_config_with_policy(policy);
3071
3072        let config = get_config().unwrap();
3073        let validator = TransactionValidator::new(config, fee_payer).unwrap();
3074
3075        let message = VersionedMessage::Legacy(Message::new(
3076            &[revoke_ix.clone(), burn_ix.clone()],
3077            Some(&fee_payer),
3078        ));
3079        let mut transaction =
3080            TransactionUtil::new_unsigned_versioned_transaction_resolved(message).unwrap();
3081        let result = validator.validate_transaction(config, &mut transaction, &rpc_client).await;
3082        if let Err(KoraError::InvalidTransaction(msg)) = result {
3083            assert!(msg.contains("Fee payer cannot be used for"));
3084        } else {
3085            panic!("Expected InvalidTransaction error for revoke policy");
3086        }
3087    }
3088}