solana_account_decoder/
parse_token.rs

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