Skip to main content

solana_account_decoder/
parse_token.rs

1use {
2    crate::{
3        parse_account_data::{ParsableAccount, ParseAccountError, SplTokenAdditionalDataV2},
4        parse_token_extension::parse_extension,
5    },
6    solana_program_option::COption,
7    solana_program_pack::Pack,
8    solana_pubkey::Pubkey,
9    spl_token_2022_interface::{
10        extension::{BaseStateWithExtensions, StateWithExtensions},
11        generic_token_account::GenericTokenAccount,
12        state::{Account, AccountState, Mint, Multisig},
13    },
14    std::str::FromStr,
15};
16pub use {
17    solana_account_decoder_client_types::token::{
18        TokenAccountType, UiAccountState, UiMint, UiMultisig, UiTokenAccount, UiTokenAmount,
19        real_number_string, real_number_string_trimmed,
20    },
21    spl_generic_token::{is_known_spl_token_id, spl_token_ids},
22};
23
24pub fn parse_token_v3(
25    data: &[u8],
26    additional_data: Option<&SplTokenAdditionalDataV2>,
27) -> Result<TokenAccountType, ParseAccountError> {
28    if let Ok(account) = StateWithExtensions::<Account>::unpack(data) {
29        let additional_data = additional_data.as_ref().ok_or_else(|| {
30            ParseAccountError::AdditionalDataMissing(
31                "no mint_decimals provided to parse spl-token account".to_string(),
32            )
33        })?;
34        let extension_types = account.get_extension_types().unwrap_or_default();
35        let ui_extensions = extension_types
36            .iter()
37            .map(|extension_type| parse_extension::<Account>(extension_type, &account))
38            .collect();
39        return Ok(TokenAccountType::Account(UiTokenAccount {
40            mint: account.base.mint.to_string(),
41            owner: account.base.owner.to_string(),
42            token_amount: token_amount_to_ui_amount_v3(account.base.amount, additional_data),
43            delegate: match account.base.delegate {
44                COption::Some(pubkey) => Some(pubkey.to_string()),
45                COption::None => None,
46            },
47            state: convert_account_state(account.base.state),
48            is_native: account.base.is_native(),
49            rent_exempt_reserve: match account.base.is_native {
50                COption::Some(reserve) => {
51                    Some(token_amount_to_ui_amount_v3(reserve, additional_data))
52                }
53                COption::None => None,
54            },
55            delegated_amount: if account.base.delegate.is_none() {
56                None
57            } else {
58                Some(token_amount_to_ui_amount_v3(
59                    account.base.delegated_amount,
60                    additional_data,
61                ))
62            },
63            close_authority: match account.base.close_authority {
64                COption::Some(pubkey) => Some(pubkey.to_string()),
65                COption::None => None,
66            },
67            extensions: ui_extensions,
68        }));
69    }
70    if let Ok(mint) = StateWithExtensions::<Mint>::unpack(data) {
71        let extension_types = mint.get_extension_types().unwrap_or_default();
72        let ui_extensions = extension_types
73            .iter()
74            .map(|extension_type| parse_extension::<Mint>(extension_type, &mint))
75            .collect();
76        return Ok(TokenAccountType::Mint(UiMint {
77            mint_authority: match mint.base.mint_authority {
78                COption::Some(pubkey) => Some(pubkey.to_string()),
79                COption::None => None,
80            },
81            supply: mint.base.supply.to_string(),
82            decimals: mint.base.decimals,
83            is_initialized: mint.base.is_initialized,
84            freeze_authority: match mint.base.freeze_authority {
85                COption::Some(pubkey) => Some(pubkey.to_string()),
86                COption::None => None,
87            },
88            extensions: ui_extensions,
89        }));
90    }
91    if data.len() == Multisig::get_packed_len() {
92        let multisig = Multisig::unpack(data)
93            .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
94        Ok(TokenAccountType::Multisig(UiMultisig {
95            num_required_signers: multisig.m,
96            num_valid_signers: multisig.n,
97            is_initialized: multisig.is_initialized,
98            signers: multisig
99                .signers
100                .iter()
101                .filter_map(|pubkey| {
102                    if pubkey != &Pubkey::default() {
103                        Some(pubkey.to_string())
104                    } else {
105                        None
106                    }
107                })
108                .collect(),
109        }))
110    } else {
111        Err(ParseAccountError::AccountNotParsable(
112            ParsableAccount::SplToken,
113        ))
114    }
115}
116
117pub fn convert_account_state(state: AccountState) -> UiAccountState {
118    match state {
119        AccountState::Uninitialized => UiAccountState::Uninitialized,
120        AccountState::Initialized => UiAccountState::Initialized,
121        AccountState::Frozen => UiAccountState::Frozen,
122    }
123}
124
125pub fn token_amount_to_ui_amount_v3(
126    amount: u64,
127    additional_data: &SplTokenAdditionalDataV2,
128) -> UiTokenAmount {
129    let decimals = additional_data.decimals;
130    let (ui_amount, ui_amount_string) = if let Some((interest_bearing_config, unix_timestamp)) =
131        additional_data.interest_bearing_config
132    {
133        let ui_amount_string =
134            interest_bearing_config.amount_to_ui_amount(amount, decimals, unix_timestamp);
135        (
136            ui_amount_string
137                .as_ref()
138                .and_then(|x| f64::from_str(x).ok()),
139            ui_amount_string.unwrap_or("".to_string()),
140        )
141    } else if let Some((scaled_ui_amount_config, unix_timestamp)) =
142        additional_data.scaled_ui_amount_config
143    {
144        let ui_amount_string =
145            scaled_ui_amount_config.amount_to_ui_amount(amount, decimals, unix_timestamp);
146        (
147            ui_amount_string
148                .as_ref()
149                .and_then(|x| f64::from_str(x).ok()),
150            ui_amount_string.unwrap_or("".to_string()),
151        )
152    } else {
153        let ui_amount = 10_usize
154            .checked_pow(decimals as u32)
155            .map(|dividend| amount as f64 / dividend as f64);
156        (ui_amount, real_number_string_trimmed(amount, decimals))
157    };
158    UiTokenAmount {
159        ui_amount,
160        decimals,
161        amount: amount.to_string(),
162        ui_amount_string,
163    }
164}
165
166pub fn get_token_account_mint(data: &[u8]) -> Option<Pubkey> {
167    Account::valid_account_data(data)
168        .then(|| Pubkey::try_from(data.get(..32)?).ok())
169        .flatten()
170}
171
172#[cfg(test)]
173mod test {
174    use {
175        super::*,
176        crate::parse_token_extension::{
177            UiMemoTransfer, UiMintCloseAuthority, UiPermissionedBurnConfig,
178        },
179        solana_account_decoder_client_types::token::UiExtension,
180        spl_token_2022_interface::extension::{
181            BaseStateWithExtensionsMut, ExtensionType, StateWithExtensionsMut,
182            immutable_owner::ImmutableOwner, interest_bearing_mint::InterestBearingConfig,
183            memo_transfer::MemoTransfer, mint_close_authority::MintCloseAuthority,
184            permissioned_burn::PermissionedBurnConfig, scaled_ui_amount::ScaledUiAmountConfig,
185        },
186    };
187
188    const INT_SECONDS_PER_YEAR: i64 = 6 * 6 * 24 * 36524;
189
190    #[test]
191    fn test_parse_token() {
192        let mint_pubkey = Pubkey::new_from_array([2; 32]);
193        let owner_pubkey = Pubkey::new_from_array([3; 32]);
194        let mut account_data = vec![0; Account::get_packed_len()];
195        let mut account = Account::unpack_unchecked(&account_data).unwrap();
196        account.mint = mint_pubkey;
197        account.owner = owner_pubkey;
198        account.amount = 42;
199        account.state = AccountState::Initialized;
200        account.is_native = COption::None;
201        account.close_authority = COption::Some(owner_pubkey);
202        Account::pack(account, &mut account_data).unwrap();
203
204        assert!(parse_token_v3(&account_data, None).is_err());
205        assert_eq!(
206            parse_token_v3(
207                &account_data,
208                Some(&SplTokenAdditionalDataV2::with_decimals(2))
209            )
210            .unwrap(),
211            TokenAccountType::Account(UiTokenAccount {
212                mint: mint_pubkey.to_string(),
213                owner: owner_pubkey.to_string(),
214                token_amount: UiTokenAmount {
215                    ui_amount: Some(0.42),
216                    decimals: 2,
217                    amount: "42".to_string(),
218                    ui_amount_string: "0.42".to_string()
219                },
220                delegate: None,
221                state: UiAccountState::Initialized,
222                is_native: false,
223                rent_exempt_reserve: None,
224                delegated_amount: None,
225                close_authority: Some(owner_pubkey.to_string()),
226                extensions: vec![],
227            }),
228        );
229
230        let mut mint_data = vec![0; Mint::get_packed_len()];
231        let mut mint = Mint::unpack_unchecked(&mint_data).unwrap();
232        mint.mint_authority = COption::Some(owner_pubkey);
233        mint.supply = 42;
234        mint.decimals = 3;
235        mint.is_initialized = true;
236        mint.freeze_authority = COption::Some(owner_pubkey);
237        Mint::pack(mint, &mut mint_data).unwrap();
238
239        assert_eq!(
240            parse_token_v3(&mint_data, None).unwrap(),
241            TokenAccountType::Mint(UiMint {
242                mint_authority: Some(owner_pubkey.to_string()),
243                supply: 42.to_string(),
244                decimals: 3,
245                is_initialized: true,
246                freeze_authority: Some(owner_pubkey.to_string()),
247                extensions: vec![],
248            }),
249        );
250
251        let signer1 = Pubkey::new_from_array([1; 32]);
252        let signer2 = Pubkey::new_from_array([2; 32]);
253        let signer3 = Pubkey::new_from_array([3; 32]);
254        let mut multisig_data = vec![0; Multisig::get_packed_len()];
255        let mut signers = [Pubkey::default(); 11];
256        signers[0] = signer1;
257        signers[1] = signer2;
258        signers[2] = signer3;
259        let mut multisig = Multisig::unpack_unchecked(&multisig_data).unwrap();
260        multisig.m = 2;
261        multisig.n = 3;
262        multisig.is_initialized = true;
263        multisig.signers = signers;
264        Multisig::pack(multisig, &mut multisig_data).unwrap();
265
266        assert_eq!(
267            parse_token_v3(&multisig_data, None).unwrap(),
268            TokenAccountType::Multisig(UiMultisig {
269                num_required_signers: 2,
270                num_valid_signers: 3,
271                is_initialized: true,
272                signers: vec![
273                    signer1.to_string(),
274                    signer2.to_string(),
275                    signer3.to_string()
276                ],
277            }),
278        );
279
280        let bad_data = vec![0; 4];
281        assert!(parse_token_v3(&bad_data, None).is_err());
282    }
283
284    #[test]
285    fn test_get_token_account_mint() {
286        let mint_pubkey = Pubkey::new_from_array([2; 32]);
287        let mut account_data = vec![0; Account::get_packed_len()];
288        let mut account = Account::unpack_unchecked(&account_data).unwrap();
289        account.mint = mint_pubkey;
290        account.state = AccountState::Initialized;
291        Account::pack(account, &mut account_data).unwrap();
292
293        let expected_mint_pubkey = Pubkey::from([2; 32]);
294        assert_eq!(
295            get_token_account_mint(&account_data),
296            Some(expected_mint_pubkey)
297        );
298    }
299
300    #[test]
301    fn test_ui_token_amount_real_string() {
302        assert_eq!(&real_number_string(1, 0), "1");
303        assert_eq!(&real_number_string_trimmed(1, 0), "1");
304        let token_amount =
305            token_amount_to_ui_amount_v3(1, &SplTokenAdditionalDataV2::with_decimals(0));
306        assert_eq!(
307            token_amount.ui_amount_string,
308            real_number_string_trimmed(1, 0)
309        );
310        assert_eq!(token_amount.ui_amount, Some(1.0));
311        assert_eq!(&real_number_string(10, 0), "10");
312        assert_eq!(&real_number_string_trimmed(10, 0), "10");
313        let token_amount =
314            token_amount_to_ui_amount_v3(10, &SplTokenAdditionalDataV2::with_decimals(0));
315        assert_eq!(
316            token_amount.ui_amount_string,
317            real_number_string_trimmed(10, 0)
318        );
319        assert_eq!(token_amount.ui_amount, Some(10.0));
320        assert_eq!(&real_number_string(1, 9), "0.000000001");
321        assert_eq!(&real_number_string_trimmed(1, 9), "0.000000001");
322        let token_amount =
323            token_amount_to_ui_amount_v3(1, &SplTokenAdditionalDataV2::with_decimals(9));
324        assert_eq!(
325            token_amount.ui_amount_string,
326            real_number_string_trimmed(1, 9)
327        );
328        assert_eq!(token_amount.ui_amount, Some(0.000000001));
329        assert_eq!(&real_number_string(1_000_000_000, 9), "1.000000000");
330        assert_eq!(&real_number_string_trimmed(1_000_000_000, 9), "1");
331        let token_amount = token_amount_to_ui_amount_v3(
332            1_000_000_000,
333            &SplTokenAdditionalDataV2::with_decimals(9),
334        );
335        assert_eq!(
336            token_amount.ui_amount_string,
337            real_number_string_trimmed(1_000_000_000, 9)
338        );
339        assert_eq!(token_amount.ui_amount, Some(1.0));
340        assert_eq!(&real_number_string(1_234_567_890, 3), "1234567.890");
341        assert_eq!(&real_number_string_trimmed(1_234_567_890, 3), "1234567.89");
342        let token_amount = token_amount_to_ui_amount_v3(
343            1_234_567_890,
344            &SplTokenAdditionalDataV2::with_decimals(3),
345        );
346        assert_eq!(
347            token_amount.ui_amount_string,
348            real_number_string_trimmed(1_234_567_890, 3)
349        );
350        assert_eq!(token_amount.ui_amount, Some(1234567.89));
351        assert_eq!(
352            &real_number_string(1_234_567_890, 25),
353            "0.0000000000000001234567890"
354        );
355        assert_eq!(
356            &real_number_string_trimmed(1_234_567_890, 25),
357            "0.000000000000000123456789"
358        );
359        let token_amount = token_amount_to_ui_amount_v3(
360            1_234_567_890,
361            &SplTokenAdditionalDataV2::with_decimals(20),
362        );
363        assert_eq!(
364            token_amount.ui_amount_string,
365            real_number_string_trimmed(1_234_567_890, 20)
366        );
367        assert_eq!(token_amount.ui_amount, None);
368    }
369
370    #[test]
371    fn test_ui_token_amount_with_interest() {
372        // constant 5%
373        let config = InterestBearingConfig {
374            initialization_timestamp: 0.into(),
375            pre_update_average_rate: 500.into(),
376            last_update_timestamp: INT_SECONDS_PER_YEAR.into(),
377            current_rate: 500.into(),
378            ..Default::default()
379        };
380        let additional_data = SplTokenAdditionalDataV2 {
381            decimals: 18,
382            interest_bearing_config: Some((config, INT_SECONDS_PER_YEAR)),
383            ..Default::default()
384        };
385        const ONE: u64 = 1_000_000_000_000_000_000;
386        const TEN: u64 = 10_000_000_000_000_000_000;
387        let token_amount = token_amount_to_ui_amount_v3(ONE, &additional_data);
388        assert!(
389            token_amount
390                .ui_amount_string
391                .starts_with("1.051271096376024117")
392        );
393        assert!((token_amount.ui_amount.unwrap() - 1.0512710963760241f64).abs() < f64::EPSILON);
394        let token_amount = token_amount_to_ui_amount_v3(TEN, &additional_data);
395        assert!(
396            token_amount
397                .ui_amount_string
398                .starts_with("10.512710963760241611")
399        );
400        assert!((token_amount.ui_amount.unwrap() - 10.512710963760242f64).abs() < f64::EPSILON);
401
402        // huge case
403        let config = InterestBearingConfig {
404            initialization_timestamp: 0.into(),
405            pre_update_average_rate: 32767.into(),
406            last_update_timestamp: 0.into(),
407            current_rate: 32767.into(),
408            ..Default::default()
409        };
410        let additional_data = SplTokenAdditionalDataV2 {
411            decimals: 0,
412            interest_bearing_config: Some((config, INT_SECONDS_PER_YEAR * 1_000)),
413            ..Default::default()
414        };
415        let token_amount = token_amount_to_ui_amount_v3(u64::MAX, &additional_data);
416        assert_eq!(token_amount.ui_amount, Some(f64::INFINITY));
417        assert_eq!(token_amount.ui_amount_string, "inf");
418    }
419
420    #[test]
421    fn test_ui_token_amount_with_multiplier() {
422        // 2x multiplier
423        let config = ScaledUiAmountConfig {
424            new_multiplier: 2f64.into(),
425            ..Default::default()
426        };
427        let additional_data = SplTokenAdditionalDataV2 {
428            decimals: 18,
429            scaled_ui_amount_config: Some((config, 0)),
430            ..Default::default()
431        };
432        const ONE: u64 = 1_000_000_000_000_000_000;
433        const TEN: u64 = 10_000_000_000_000_000_000;
434        let token_amount = token_amount_to_ui_amount_v3(ONE, &additional_data);
435        assert_eq!(token_amount.ui_amount_string, "2");
436        assert!(token_amount.ui_amount_string.starts_with("2"));
437        assert!((token_amount.ui_amount.unwrap() - 2.0).abs() < f64::EPSILON);
438        let token_amount = token_amount_to_ui_amount_v3(TEN, &additional_data);
439        assert!(token_amount.ui_amount_string.starts_with("20"));
440        assert!((token_amount.ui_amount.unwrap() - 20.0).abs() < f64::EPSILON);
441
442        // huge case
443        let config = ScaledUiAmountConfig {
444            new_multiplier: f64::INFINITY.into(),
445            ..Default::default()
446        };
447        let additional_data = SplTokenAdditionalDataV2 {
448            decimals: 0,
449            scaled_ui_amount_config: Some((config, 0)),
450            ..Default::default()
451        };
452        let token_amount = token_amount_to_ui_amount_v3(u64::MAX, &additional_data);
453        assert_eq!(token_amount.ui_amount, Some(f64::INFINITY));
454        assert_eq!(token_amount.ui_amount_string, "inf");
455    }
456
457    #[test]
458    fn test_ui_token_amount_real_string_zero() {
459        assert_eq!(&real_number_string(0, 0), "0");
460        assert_eq!(&real_number_string_trimmed(0, 0), "0");
461        let token_amount =
462            token_amount_to_ui_amount_v3(0, &SplTokenAdditionalDataV2::with_decimals(0));
463        assert_eq!(
464            token_amount.ui_amount_string,
465            real_number_string_trimmed(0, 0)
466        );
467        assert_eq!(token_amount.ui_amount, Some(0.0));
468        assert_eq!(&real_number_string(0, 9), "0.000000000");
469        assert_eq!(&real_number_string_trimmed(0, 9), "0");
470        let token_amount =
471            token_amount_to_ui_amount_v3(0, &SplTokenAdditionalDataV2::with_decimals(9));
472        assert_eq!(
473            token_amount.ui_amount_string,
474            real_number_string_trimmed(0, 9)
475        );
476        assert_eq!(token_amount.ui_amount, Some(0.0));
477        assert_eq!(&real_number_string(0, 25), "0.0000000000000000000000000");
478        assert_eq!(&real_number_string_trimmed(0, 25), "0");
479        let token_amount =
480            token_amount_to_ui_amount_v3(0, &SplTokenAdditionalDataV2::with_decimals(20));
481        assert_eq!(
482            token_amount.ui_amount_string,
483            real_number_string_trimmed(0, 20)
484        );
485        assert_eq!(token_amount.ui_amount, None);
486    }
487
488    #[test]
489    fn test_parse_token_account_with_extensions() {
490        let mint_pubkey = Pubkey::new_from_array([2; 32]);
491        let owner_pubkey = Pubkey::new_from_array([3; 32]);
492
493        let account_base = Account {
494            mint: mint_pubkey,
495            owner: owner_pubkey,
496            amount: 42,
497            state: AccountState::Initialized,
498            is_native: COption::None,
499            close_authority: COption::Some(owner_pubkey),
500            delegate: COption::None,
501            delegated_amount: 0,
502        };
503        let account_size = ExtensionType::try_calculate_account_len::<Account>(&[
504            ExtensionType::ImmutableOwner,
505            ExtensionType::MemoTransfer,
506        ])
507        .unwrap();
508        let mut account_data = vec![0; account_size];
509        let mut account_state =
510            StateWithExtensionsMut::<Account>::unpack_uninitialized(&mut account_data).unwrap();
511
512        account_state.base = account_base;
513        account_state.pack_base();
514        account_state.init_account_type().unwrap();
515
516        assert!(parse_token_v3(&account_data, None).is_err());
517        assert_eq!(
518            parse_token_v3(
519                &account_data,
520                Some(&SplTokenAdditionalDataV2::with_decimals(2))
521            )
522            .unwrap(),
523            TokenAccountType::Account(UiTokenAccount {
524                mint: mint_pubkey.to_string(),
525                owner: owner_pubkey.to_string(),
526                token_amount: UiTokenAmount {
527                    ui_amount: Some(0.42),
528                    decimals: 2,
529                    amount: "42".to_string(),
530                    ui_amount_string: "0.42".to_string()
531                },
532                delegate: None,
533                state: UiAccountState::Initialized,
534                is_native: false,
535                rent_exempt_reserve: None,
536                delegated_amount: None,
537                close_authority: Some(owner_pubkey.to_string()),
538                extensions: vec![],
539            }),
540        );
541
542        let mut account_data = vec![0; account_size];
543        let mut account_state =
544            StateWithExtensionsMut::<Account>::unpack_uninitialized(&mut account_data).unwrap();
545
546        account_state.base = account_base;
547        account_state.pack_base();
548        account_state.init_account_type().unwrap();
549
550        account_state
551            .init_extension::<ImmutableOwner>(true)
552            .unwrap();
553        let memo_transfer = account_state.init_extension::<MemoTransfer>(true).unwrap();
554        memo_transfer.require_incoming_transfer_memos = true.into();
555
556        assert!(parse_token_v3(&account_data, None).is_err());
557        assert_eq!(
558            parse_token_v3(
559                &account_data,
560                Some(&SplTokenAdditionalDataV2::with_decimals(2))
561            )
562            .unwrap(),
563            TokenAccountType::Account(UiTokenAccount {
564                mint: mint_pubkey.to_string(),
565                owner: owner_pubkey.to_string(),
566                token_amount: UiTokenAmount {
567                    ui_amount: Some(0.42),
568                    decimals: 2,
569                    amount: "42".to_string(),
570                    ui_amount_string: "0.42".to_string()
571                },
572                delegate: None,
573                state: UiAccountState::Initialized,
574                is_native: false,
575                rent_exempt_reserve: None,
576                delegated_amount: None,
577                close_authority: Some(owner_pubkey.to_string()),
578                extensions: vec![
579                    UiExtension::ImmutableOwner,
580                    UiExtension::MemoTransfer(UiMemoTransfer {
581                        require_incoming_transfer_memos: true,
582                    }),
583                ],
584            }),
585        );
586    }
587
588    #[test]
589    fn test_parse_token_mint_with_extensions() {
590        let owner_pubkey = Pubkey::new_from_array([3; 32]);
591        let mint_size =
592            ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::MintCloseAuthority])
593                .unwrap();
594        let mint_base = Mint {
595            mint_authority: COption::Some(owner_pubkey),
596            supply: 42,
597            decimals: 3,
598            is_initialized: true,
599            freeze_authority: COption::Some(owner_pubkey),
600        };
601        let mut mint_data = vec![0; mint_size];
602        let mut mint_state =
603            StateWithExtensionsMut::<Mint>::unpack_uninitialized(&mut mint_data).unwrap();
604
605        mint_state.base = mint_base;
606        mint_state.pack_base();
607        mint_state.init_account_type().unwrap();
608
609        assert_eq!(
610            parse_token_v3(&mint_data, None).unwrap(),
611            TokenAccountType::Mint(UiMint {
612                mint_authority: Some(owner_pubkey.to_string()),
613                supply: 42.to_string(),
614                decimals: 3,
615                is_initialized: true,
616                freeze_authority: Some(owner_pubkey.to_string()),
617                extensions: vec![],
618            }),
619        );
620
621        let mut mint_data = vec![0; mint_size];
622        let mut mint_state =
623            StateWithExtensionsMut::<Mint>::unpack_uninitialized(&mut mint_data).unwrap();
624
625        let mint_close_authority = mint_state
626            .init_extension::<MintCloseAuthority>(true)
627            .unwrap();
628        mint_close_authority.close_authority = owner_pubkey.into();
629
630        mint_state.base = mint_base;
631        mint_state.pack_base();
632        mint_state.init_account_type().unwrap();
633
634        assert_eq!(
635            parse_token_v3(&mint_data, None).unwrap(),
636            TokenAccountType::Mint(UiMint {
637                mint_authority: Some(owner_pubkey.to_string()),
638                supply: 42.to_string(),
639                decimals: 3,
640                is_initialized: true,
641                freeze_authority: Some(owner_pubkey.to_string()),
642                extensions: vec![UiExtension::MintCloseAuthority(UiMintCloseAuthority {
643                    close_authority: Some(owner_pubkey.to_string()),
644                })],
645            }),
646        );
647    }
648
649    #[test]
650    fn test_parse_token_mint_with_permissioned_burn() {
651        let owner_pubkey = Pubkey::new_from_array([3; 32]);
652        let authority_pubkey = Pubkey::new_from_array([4; 32]);
653        let mint_size =
654            ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::PermissionedBurn])
655                .unwrap();
656        let mint_base = Mint {
657            mint_authority: COption::Some(owner_pubkey),
658            supply: 42,
659            decimals: 3,
660            is_initialized: true,
661            freeze_authority: COption::Some(owner_pubkey),
662        };
663        let mut mint_data = vec![0; mint_size];
664        let mut mint_state =
665            StateWithExtensionsMut::<Mint>::unpack_uninitialized(&mut mint_data).unwrap();
666
667        let permissioned_burn = mint_state
668            .init_extension::<PermissionedBurnConfig>(true)
669            .unwrap();
670        permissioned_burn.authority = authority_pubkey.into();
671
672        mint_state.base = mint_base;
673        mint_state.pack_base();
674        mint_state.init_account_type().unwrap();
675
676        assert_eq!(
677            parse_token_v3(&mint_data, None).unwrap(),
678            TokenAccountType::Mint(UiMint {
679                mint_authority: Some(owner_pubkey.to_string()),
680                supply: 42.to_string(),
681                decimals: 3,
682                is_initialized: true,
683                freeze_authority: Some(owner_pubkey.to_string()),
684                extensions: vec![UiExtension::PermissionedBurnConfig(
685                    UiPermissionedBurnConfig {
686                        authority: Some(authority_pubkey.to_string()),
687                    }
688                )],
689            }),
690        );
691    }
692}