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
22pub fn spl_token_ids() -> Vec<Pubkey> {
24 vec![spl_token::id(), spl_token_2022::id()]
25}
26
27pub 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 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 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}