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