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