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