solana_account_decoder_wasm/
parse_token.rs

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