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