kora_lib/validator/
transaction_validator.rs

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