Skip to main content

kora_lib/token/
spl_token_2022.rs

1use crate::token::{
2    interface::TokenMint,
3    spl_token_2022_util::{
4        try_parse_account_extension, try_parse_mint_extension, AccountExtension, MintExtension,
5        ParsedExtension,
6    },
7};
8
9use super::interface::{TokenInterface, TokenState};
10use async_trait::async_trait;
11use solana_program::{program_pack::Pack, pubkey::Pubkey};
12use solana_sdk::instruction::Instruction;
13use spl_associated_token_account_interface::{
14    address::get_associated_token_address_with_program_id,
15    instruction::create_associated_token_account,
16};
17use spl_token_2022_interface::{
18    extension::{transfer_fee::TransferFeeConfig, ExtensionType, StateWithExtensions},
19    state::{Account as Token2022AccountState, AccountState, Mint as Token2022MintState},
20};
21use std::{collections::HashMap, fmt::Debug};
22
23#[derive(Debug)]
24pub struct Token2022Account {
25    pub mint: Pubkey,
26    pub owner: Pubkey,
27    pub amount: u64,
28    pub delegate: Option<Pubkey>,
29    pub state: u8,
30    pub is_native: Option<u64>,
31    pub delegated_amount: u64,
32    pub close_authority: Option<Pubkey>,
33    // Extensions types present on the account (used for speed when we don't need the data of the actual extensions)
34    pub extensions_types: Vec<ExtensionType>,
35    /// Parsed extension data stored by extension type discriminant
36    pub extensions: HashMap<u16, ParsedExtension>,
37}
38
39impl TokenState for Token2022Account {
40    fn mint(&self) -> Pubkey {
41        self.mint
42    }
43    fn owner(&self) -> Pubkey {
44        self.owner
45    }
46    fn amount(&self) -> u64 {
47        self.amount
48    }
49    fn decimals(&self) -> u8 {
50        0
51    }
52    fn as_any(&self) -> &dyn std::any::Any {
53        self
54    }
55}
56
57impl Token2022Account {
58    /*
59    Token account only extensions
60     */
61    pub fn has_memo_extension(&self) -> bool {
62        self.has_extension(ExtensionType::MemoTransfer)
63    }
64
65    pub fn has_immutable_owner_extension(&self) -> bool {
66        self.has_extension(ExtensionType::ImmutableOwner)
67    }
68
69    pub fn has_default_account_state_extension(&self) -> bool {
70        self.has_extension(ExtensionType::DefaultAccountState)
71    }
72}
73
74impl Token2022Extensions for Token2022Account {
75    fn get_extensions(&self) -> &HashMap<u16, ParsedExtension> {
76        &self.extensions
77    }
78
79    fn get_extension_types(&self) -> &Vec<ExtensionType> {
80        &self.extensions_types
81    }
82
83    /*
84    Token account & mint account extensions (each their own type)
85     */
86
87    fn has_confidential_transfer_extension(&self) -> bool {
88        self.has_extension(ExtensionType::ConfidentialTransferAccount)
89    }
90
91    fn has_transfer_hook_extension(&self) -> bool {
92        self.has_extension(ExtensionType::TransferHookAccount)
93    }
94
95    fn has_pausable_extension(&self) -> bool {
96        self.has_extension(ExtensionType::PausableAccount)
97    }
98
99    fn is_non_transferable(&self) -> bool {
100        self.has_extension(ExtensionType::NonTransferableAccount)
101    }
102}
103
104#[derive(Debug)]
105pub struct Token2022Mint {
106    pub mint: Pubkey,
107    pub mint_authority: Option<Pubkey>,
108    pub supply: u64,
109    pub decimals: u8,
110    pub is_initialized: bool,
111    pub freeze_authority: Option<Pubkey>,
112    // Extensions types present on the mint (used for speed when we don't need the data of the actual extensions)
113    pub extensions_types: Vec<ExtensionType>,
114    /// Parsed extension data stored by extension type discriminant
115    pub extensions: HashMap<u16, ParsedExtension>,
116}
117
118impl TokenMint for Token2022Mint {
119    fn address(&self) -> Pubkey {
120        self.mint
121    }
122
123    fn decimals(&self) -> u8 {
124        self.decimals
125    }
126
127    fn mint_authority(&self) -> Option<Pubkey> {
128        self.mint_authority
129    }
130
131    fn supply(&self) -> u64 {
132        self.supply
133    }
134
135    fn freeze_authority(&self) -> Option<Pubkey> {
136        self.freeze_authority
137    }
138
139    fn is_initialized(&self) -> bool {
140        self.is_initialized
141    }
142
143    fn get_token_program(&self) -> Box<dyn TokenInterface> {
144        Box::new(Token2022Program::new())
145    }
146
147    fn as_any(&self) -> &dyn std::any::Any {
148        self
149    }
150}
151
152impl Token2022Mint {
153    fn get_transfer_fee(&self) -> Option<TransferFeeConfig> {
154        match self.get_extension(ExtensionType::TransferFeeConfig) {
155            Some(ParsedExtension::Mint(MintExtension::TransferFeeConfig(config))) => Some(*config),
156            _ => None,
157        }
158    }
159
160    /// Calculate transfer fee for a given amount and epoch
161    /// Returns None if no transfer fee is configured
162    pub fn calculate_transfer_fee(
163        &self,
164        amount: u64,
165        current_epoch: u64,
166    ) -> Result<Option<u64>, crate::error::KoraError> {
167        if let Some(fee_config) = self.get_transfer_fee() {
168            let transfer_fee = if current_epoch >= u64::from(fee_config.newer_transfer_fee.epoch) {
169                &fee_config.newer_transfer_fee
170            } else {
171                &fee_config.older_transfer_fee
172            };
173
174            let basis_points = u16::from(transfer_fee.transfer_fee_basis_points);
175            let maximum_fee = u64::from(transfer_fee.maximum_fee);
176
177            let fee_amount = (amount as u128)
178                .checked_mul(basis_points as u128)
179                .and_then(|product| product.checked_add(10_000 - 1))
180                .and_then(|product| product.checked_div(10_000))
181                .and_then(
182                    |result| if result <= u64::MAX as u128 { Some(result as u64) } else { None },
183                )
184                .ok_or_else(|| {
185                    log::error!(
186                        "Transfer fee calculation overflow: amount={}, basis_points={}",
187                        amount,
188                        basis_points
189                    );
190                    crate::error::KoraError::ValidationError(format!(
191                        "Transfer fee calculation overflow: amount={}, basis_points={}",
192                        amount, basis_points
193                    ))
194                })?;
195            Ok(Some(std::cmp::min(fee_amount, maximum_fee)))
196        } else {
197            Ok(None)
198        }
199    }
200
201    pub fn has_confidential_mint_burn_extension(&self) -> bool {
202        self.has_extension(ExtensionType::ConfidentialMintBurn)
203    }
204
205    pub fn has_mint_close_authority_extension(&self) -> bool {
206        self.has_extension(ExtensionType::MintCloseAuthority)
207    }
208
209    pub fn has_interest_bearing_extension(&self) -> bool {
210        self.has_extension(ExtensionType::InterestBearingConfig)
211    }
212
213    pub fn has_permanent_delegate_extension(&self) -> bool {
214        self.has_extension(ExtensionType::PermanentDelegate)
215    }
216}
217
218impl Token2022Extensions for Token2022Mint {
219    fn get_extensions(&self) -> &HashMap<u16, ParsedExtension> {
220        &self.extensions
221    }
222
223    fn get_extension_types(&self) -> &Vec<ExtensionType> {
224        &self.extensions_types
225    }
226
227    /*
228    Token account & mint account extensions (each their own type)
229     */
230
231    fn has_confidential_transfer_extension(&self) -> bool {
232        self.has_extension(ExtensionType::ConfidentialTransferMint)
233    }
234
235    fn has_transfer_hook_extension(&self) -> bool {
236        self.has_extension(ExtensionType::TransferHook)
237    }
238
239    fn has_pausable_extension(&self) -> bool {
240        self.has_extension(ExtensionType::Pausable)
241    }
242
243    fn is_non_transferable(&self) -> bool {
244        self.has_extension(ExtensionType::NonTransferable)
245    }
246}
247
248pub struct Token2022Program;
249
250impl Token2022Program {
251    pub fn new() -> Self {
252        Self
253    }
254}
255
256impl Default for Token2022Program {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262#[async_trait]
263impl TokenInterface for Token2022Program {
264    fn program_id(&self) -> Pubkey {
265        spl_token_2022_interface::id()
266    }
267
268    fn unpack_token_account(
269        &self,
270        data: &[u8],
271    ) -> Result<Box<dyn TokenState + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
272        let account = StateWithExtensions::<Token2022AccountState>::unpack(data)?;
273        let base = account.base;
274
275        // Parse all extensions and store in HashMap
276        let mut extensions = HashMap::new();
277        let mut extensions_types = Vec::new();
278
279        if data.len() > Token2022AccountState::LEN {
280            for &extension_type in AccountExtension::EXTENSIONS {
281                if let Some(parsed_ext) = try_parse_account_extension(&account, extension_type) {
282                    extensions.insert(extension_type as u16, parsed_ext);
283                    extensions_types.push(extension_type);
284                }
285            }
286        }
287
288        Ok(Box::new(Token2022Account {
289            mint: base.mint,
290            owner: base.owner,
291            amount: base.amount,
292            delegate: base.delegate.into(),
293            state: match base.state {
294                AccountState::Uninitialized => 0,
295                AccountState::Initialized => 1,
296                AccountState::Frozen => 2,
297            },
298            is_native: base.is_native.into(),
299            delegated_amount: base.delegated_amount,
300            close_authority: base.close_authority.into(),
301            extensions_types,
302            extensions,
303        }))
304    }
305
306    fn create_initialize_account_instruction(
307        &self,
308        account: &Pubkey,
309        mint: &Pubkey,
310        owner: &Pubkey,
311    ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
312        Ok(spl_token_2022_interface::instruction::initialize_account3(
313            &self.program_id(),
314            account,
315            mint,
316            owner,
317        )?)
318    }
319
320    fn create_transfer_instruction(
321        &self,
322        source: &Pubkey,
323        destination: &Pubkey,
324        authority: &Pubkey,
325        amount: u64,
326    ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
327        // Get the mint from the source account data
328        #[allow(deprecated)]
329        Ok(spl_token_2022_interface::instruction::transfer(
330            &self.program_id(),
331            source,
332            destination,
333            authority,
334            &[],
335            amount,
336        )?)
337    }
338
339    fn create_transfer_checked_instruction(
340        &self,
341        source: &Pubkey,
342        mint: &Pubkey,
343        destination: &Pubkey,
344        authority: &Pubkey,
345        amount: u64,
346        decimals: u8,
347    ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
348        Ok(spl_token_2022_interface::instruction::transfer_checked(
349            &self.program_id(),
350            source,
351            mint,
352            destination,
353            authority,
354            &[],
355            amount,
356            decimals,
357        )?)
358    }
359
360    fn get_associated_token_address(&self, wallet: &Pubkey, mint: &Pubkey) -> Pubkey {
361        get_associated_token_address_with_program_id(wallet, mint, &self.program_id())
362    }
363
364    fn create_associated_token_account_instruction(
365        &self,
366        funding_account: &Pubkey,
367        wallet: &Pubkey,
368        mint: &Pubkey,
369    ) -> Instruction {
370        create_associated_token_account(funding_account, wallet, mint, &self.program_id())
371    }
372
373    fn unpack_mint(
374        &self,
375        mint: &Pubkey,
376        mint_data: &[u8],
377    ) -> Result<Box<dyn TokenMint + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
378        let mint_with_extensions = StateWithExtensions::<Token2022MintState>::unpack(mint_data)?;
379        let base = mint_with_extensions.base;
380
381        // Parse all extensions and store in HashMap
382        let mut extensions = HashMap::new();
383        let mut extensions_types = Vec::new();
384
385        if mint_data.len() > Token2022MintState::LEN {
386            for &extension_type in MintExtension::EXTENSIONS {
387                if let Some(parsed_ext) =
388                    try_parse_mint_extension(&mint_with_extensions, extension_type)
389                {
390                    extensions.insert(extension_type as u16, parsed_ext);
391                    extensions_types.push(extension_type);
392                }
393            }
394        }
395
396        Ok(Box::new(Token2022Mint {
397            mint: *mint,
398            mint_authority: base.mint_authority.into(),
399            supply: base.supply,
400            decimals: base.decimals,
401            is_initialized: base.is_initialized,
402            freeze_authority: base.freeze_authority.into(),
403            extensions_types,
404            extensions,
405        }))
406    }
407}
408
409/// Trait for Token-2022 extension validation and fee calculation
410pub trait Token2022Extensions {
411    /// Provide access to the extensions HashMap
412    fn get_extensions(&self) -> &HashMap<u16, ParsedExtension>;
413
414    /// Get all extension types
415    fn get_extension_types(&self) -> &Vec<ExtensionType>;
416
417    /// Helper function to convert ExtensionType to u16 key
418    fn extension_key(ext_type: ExtensionType) -> u16 {
419        ext_type as u16
420    }
421
422    /// Check if has a specific extension type
423    fn has_extension(&self, extension_type: ExtensionType) -> bool {
424        self.get_extension_types().contains(&extension_type)
425    }
426
427    /// Get extension by type
428    fn get_extension(&self, extension_type: ExtensionType) -> Option<&ParsedExtension> {
429        self.get_extensions().get(&Self::extension_key(extension_type))
430    }
431
432    fn has_confidential_transfer_extension(&self) -> bool;
433
434    fn has_transfer_hook_extension(&self) -> bool;
435
436    fn has_pausable_extension(&self) -> bool;
437
438    /// Check if the token/mint is non-transferable (differs between Account and Mint)
439    fn is_non_transferable(&self) -> bool;
440}
441
442#[cfg(test)]
443mod tests {
444    use crate::tests::common::{
445        create_transfer_fee_config, MintAccountMockBuilder, TokenAccountMockBuilder,
446    };
447
448    use super::*;
449    use solana_sdk::pubkey::Pubkey;
450    use spl_pod::{
451        optional_keys::OptionalNonZeroPubkey,
452        primitives::{PodU16, PodU64},
453    };
454    use spl_token_2022_interface::extension::{
455        transfer_fee::{TransferFee, TransferFeeConfig},
456        ExtensionType,
457    };
458
459    pub fn create_test_extensions() -> HashMap<u16, ParsedExtension> {
460        let mut extensions = HashMap::new();
461        extensions.insert(
462            ExtensionType::TransferFeeConfig as u16,
463            ParsedExtension::Mint(MintExtension::TransferFeeConfig(create_transfer_fee_config(
464                100, 1000,
465            ))),
466        );
467        extensions
468    }
469
470    #[test]
471    fn test_token_program_token2022() {
472        let program = Token2022Program::new();
473        assert_eq!(program.program_id(), spl_token_2022_interface::id());
474    }
475
476    #[test]
477    fn test_token2022_program_creation() {
478        let program = Token2022Program::new();
479        assert_eq!(program.program_id(), spl_token_2022_interface::id());
480    }
481
482    #[test]
483    fn test_token2022_account_state() {
484        let mint = Pubkey::new_unique();
485        let owner = Pubkey::new_unique();
486        let amount = 1000;
487
488        // Create a Token2022Account directly
489        let account = Token2022Account {
490            mint,
491            owner,
492            amount,
493            delegate: None,
494            state: 1, // Initialized
495            is_native: None,
496            delegated_amount: 0,
497            close_authority: None,
498            extensions_types: vec![ExtensionType::TransferFeeConfig],
499            extensions: create_test_extensions(),
500        };
501
502        // Verify the basic fields
503        assert_eq!(account.mint(), mint);
504        assert_eq!(account.owner(), owner);
505        assert_eq!(account.amount(), amount);
506
507        // Verify extensions map is available
508        assert!(!account.extensions.is_empty());
509    }
510
511    #[test]
512    fn test_token2022_transfer_instruction() {
513        let source = Pubkey::new_unique();
514        let dest = Pubkey::new_unique();
515        let authority = Pubkey::new_unique();
516        let amount = 100;
517
518        // Create the instruction directly for testing
519        let program = Token2022Program::new();
520        let ix = program.create_transfer_instruction(&source, &dest, &authority, amount).unwrap();
521
522        assert_eq!(ix.program_id, spl_token_2022_interface::id());
523        // Verify accounts are in correct order
524        assert_eq!(ix.accounts[0].pubkey, source);
525        assert_eq!(ix.accounts[1].pubkey, dest);
526        assert_eq!(ix.accounts[2].pubkey, authority);
527    }
528
529    #[test]
530    fn test_token2022_transfer_checked_instruction() {
531        let source = Pubkey::new_unique();
532        let dest = Pubkey::new_unique();
533        let mint = Pubkey::new_unique();
534        let authority = Pubkey::new_unique();
535        let amount = 100;
536        let decimals = 9;
537
538        let program = Token2022Program::new();
539        let ix = program
540            .create_transfer_checked_instruction(
541                &source, &mint, &dest, &authority, amount, decimals,
542            )
543            .unwrap();
544
545        assert_eq!(ix.program_id, spl_token_2022_interface::id());
546        // Verify accounts are in correct order
547        assert_eq!(ix.accounts[0].pubkey, source);
548        assert_eq!(ix.accounts[1].pubkey, mint);
549        assert_eq!(ix.accounts[2].pubkey, dest);
550        assert_eq!(ix.accounts[3].pubkey, authority);
551    }
552
553    #[test]
554    fn test_token2022_ata_derivation() {
555        let program = Token2022Program::new();
556        let wallet = Pubkey::new_unique();
557        let mint = Pubkey::new_unique();
558
559        let ata = program.get_associated_token_address(&wallet, &mint);
560
561        // Verify ATA derivation matches spl-token-2022
562        let expected_ata =
563            spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
564                &wallet,
565                &mint,
566                &spl_token_2022_interface::id(),
567            );
568        assert_eq!(ata, expected_ata);
569    }
570
571    #[test]
572    fn test_token2022_account_state_extensions() {
573        let owner = Pubkey::new_unique();
574        let mint = Pubkey::new_unique();
575        let amount = 1000;
576
577        let token_account = TokenAccountMockBuilder::new()
578            .with_mint(&mint)
579            .with_owner(&owner)
580            .with_amount(amount)
581            .build_as_custom_token2022_token_account(HashMap::new());
582
583        // Test extension detection
584        assert!(!token_account.has_extension(ExtensionType::TransferFeeConfig));
585        assert!(!token_account.has_extension(ExtensionType::NonTransferableAccount));
586        assert!(!token_account.has_extension(ExtensionType::CpiGuard));
587    }
588
589    #[test]
590    fn test_token2022_extension_support() {
591        let mint = Pubkey::new_unique();
592        let owner = Pubkey::new_unique();
593        let amount = 1000;
594
595        let token_account = TokenAccountMockBuilder::new()
596            .with_mint(&mint)
597            .with_owner(&owner)
598            .with_amount(amount)
599            .build_as_custom_token2022_token_account(create_test_extensions());
600
601        assert_eq!(token_account.mint(), mint);
602        assert_eq!(token_account.owner(), owner);
603        assert_eq!(token_account.amount(), amount);
604
605        assert!(!token_account.extensions.is_empty());
606    }
607
608    #[test]
609    fn test_token2022_mint_transfer_fee_edge_cases() {
610        let mint_pubkey = Pubkey::new_unique();
611
612        let mint = MintAccountMockBuilder::new()
613            .build_as_custom_token2022_mint(mint_pubkey, HashMap::new());
614
615        let fee = mint.calculate_transfer_fee(1000, 0).unwrap();
616        assert!(fee.is_none(), "Mint without transfer fee config should return None");
617
618        let mint = MintAccountMockBuilder::new()
619            .build_as_custom_token2022_mint(mint_pubkey, create_test_extensions());
620
621        // Test zero amount
622        let zero_fee = mint.calculate_transfer_fee(0, 0).unwrap();
623        assert!(zero_fee.is_some());
624        assert_eq!(zero_fee.unwrap(), 0, "Zero amount should result in zero fee");
625
626        // Test maximum fee cap
627        let large_amount_fee = mint.calculate_transfer_fee(1_000_000, 0).unwrap();
628        assert!(large_amount_fee.is_some());
629        assert_eq!(large_amount_fee.unwrap(), 1000, "Large amount should be capped at maximum fee");
630    }
631
632    #[test]
633    fn test_token2022_mint_specific_extensions() {
634        let mint_pubkey = Pubkey::new_unique();
635        let mint = Token2022Mint {
636            mint: mint_pubkey,
637            mint_authority: None,
638            supply: 0,
639            decimals: 6,
640            is_initialized: true,
641            freeze_authority: None,
642            extensions_types: vec![
643                ExtensionType::InterestBearingConfig,
644                ExtensionType::PermanentDelegate,
645                ExtensionType::MintCloseAuthority,
646            ],
647            extensions: HashMap::new(), // Extension data not needed for has_extension tests
648        };
649
650        assert!(mint.has_interest_bearing_extension());
651        assert!(mint.has_permanent_delegate_extension());
652        assert!(mint.has_mint_close_authority_extension());
653        assert!(!mint.has_confidential_mint_burn_extension());
654    }
655
656    #[test]
657    fn test_token2022_account_extension_methods() {
658        let account = TokenAccountMockBuilder::new()
659            .with_extensions(vec![
660                ExtensionType::MemoTransfer,
661                ExtensionType::ImmutableOwner,
662                ExtensionType::DefaultAccountState,
663                ExtensionType::ConfidentialTransferAccount,
664                ExtensionType::TransferHookAccount,
665                ExtensionType::PausableAccount,
666            ])
667            .build_as_custom_token2022_token_account(HashMap::new());
668
669        // Test all extension detection methods
670        assert!(account.has_memo_extension());
671        assert!(account.has_immutable_owner_extension());
672        assert!(account.has_default_account_state_extension());
673        assert!(account.has_confidential_transfer_extension());
674        assert!(account.has_transfer_hook_extension());
675        assert!(account.has_pausable_extension());
676
677        // Test extensions not present
678        assert!(!account.is_non_transferable());
679    }
680
681    #[test]
682    fn test_token2022_mint_transfer_fee_calculation_with_fee() {
683        let mint_pubkey = Pubkey::new_unique();
684        let mut extensions = HashMap::new();
685        extensions.insert(
686            ExtensionType::TransferFeeConfig as u16,
687            ParsedExtension::Mint(MintExtension::TransferFeeConfig(
688                crate::tests::account_mock::create_transfer_fee_config(250, 1000),
689            )), // 2.5%, max 1000
690        );
691
692        let mint = MintAccountMockBuilder::new()
693            .with_extensions(vec![ExtensionType::TransferFeeConfig])
694            .build_as_custom_token2022_mint(mint_pubkey, extensions);
695
696        // Test fee calculation with transfer fee (ceiling division to match on-chain)
697        let test_cases: Vec<(u64, u64)> = vec![
698            (10000, 250),   // ceil(10000 * 250 / 10000) = 250
699            (100000, 1000), // ceil(100000 * 250 / 10000) = 2500, capped at max 1000
700            (1000, 25),     // ceil(1000 * 250 / 10000) = 25
701            (101, 3),       // ceil(101 * 250 / 10000) = ceil(2.525) = 3
702            (0, 0),         // Zero amount = zero fee
703        ];
704
705        for (amount, expected_fee) in test_cases {
706            let fee = mint.calculate_transfer_fee(amount, 0).unwrap().unwrap_or(0);
707            assert_eq!(
708                fee, expected_fee,
709                "Fee mismatch for amount={amount}: got={fee}, expected={expected_fee}"
710            );
711        }
712    }
713
714    #[test]
715    fn test_token2022_mint_transfer_fee_epoch_handling() {
716        let mint_pubkey = Pubkey::new_unique();
717
718        // Create config with different fees for different epochs
719        let transfer_fee_config = TransferFeeConfig {
720            transfer_fee_config_authority: OptionalNonZeroPubkey::try_from(Some(
721                spl_pod::solana_pubkey::Pubkey::new_unique(),
722            ))
723            .unwrap(),
724            withdraw_withheld_authority: OptionalNonZeroPubkey::try_from(Some(
725                spl_pod::solana_pubkey::Pubkey::new_unique(),
726            ))
727            .unwrap(),
728            withheld_amount: PodU64::from(0),
729            older_transfer_fee: TransferFee {
730                epoch: PodU64::from(0),
731                transfer_fee_basis_points: PodU16::from(100), // 1%
732                maximum_fee: PodU64::from(500),
733            },
734            newer_transfer_fee: TransferFee {
735                epoch: PodU64::from(10),
736                transfer_fee_basis_points: PodU16::from(200), // 2%
737                maximum_fee: PodU64::from(1000),
738            },
739        };
740
741        let mut extensions = HashMap::new();
742        extensions.insert(
743            ExtensionType::TransferFeeConfig as u16,
744            ParsedExtension::Mint(MintExtension::TransferFeeConfig(transfer_fee_config)),
745        );
746
747        let mint = MintAccountMockBuilder::new()
748            .with_extensions(vec![ExtensionType::TransferFeeConfig])
749            .build_as_custom_token2022_mint(mint_pubkey, extensions);
750
751        // Test older fee (epoch < 10)
752        let fee_old = mint.calculate_transfer_fee(10000, 5).unwrap().unwrap();
753        assert_eq!(fee_old, 100); // 10000 * 1% = 100
754
755        // Test newer fee (epoch >= 10)
756        let fee_new = mint.calculate_transfer_fee(10000, 15).unwrap().unwrap();
757        assert_eq!(fee_new, 200); // 10000 * 2% = 200
758    }
759
760    #[test]
761    fn test_token2022_mint_all_extension_methods() {
762        let mint = MintAccountMockBuilder::new()
763            .with_extensions(vec![
764                ExtensionType::InterestBearingConfig,
765                ExtensionType::PermanentDelegate,
766                ExtensionType::MintCloseAuthority,
767                ExtensionType::ConfidentialMintBurn,
768                ExtensionType::ConfidentialTransferMint,
769                ExtensionType::TransferHook,
770                ExtensionType::Pausable,
771            ])
772            .build_as_custom_token2022_mint(Pubkey::new_unique(), HashMap::new());
773
774        // Test all extension detection methods
775        assert!(mint.has_interest_bearing_extension());
776        assert!(mint.has_permanent_delegate_extension());
777        assert!(mint.has_mint_close_authority_extension());
778        assert!(mint.has_confidential_mint_burn_extension());
779        assert!(mint.has_confidential_transfer_extension());
780        assert!(mint.has_transfer_hook_extension());
781        assert!(mint.has_pausable_extension());
782
783        // Test extensions not present
784        assert!(!mint.is_non_transferable());
785    }
786}