1use crate::{
2 parse_account_data::{ParsableAccount, ParseAccountError},
3 StringAmount, StringDecimals,
4};
5use gemachain_sdk::pubkey::Pubkey;
6use spl_token_v2_0::{
7 solana_program::{
8 program_option::COption, program_pack::Pack, pubkey::Pubkey as SplTokenPubkey,
9 },
10 state::{Account, AccountState, Mint, Multisig},
11};
12use std::str::FromStr;
13
14pub fn spl_token_id_v2_0() -> Pubkey {
17 Pubkey::new_from_array(spl_token_v2_0::id().to_bytes())
18}
19
20pub fn spl_token_v2_0_native_mint() -> Pubkey {
23 Pubkey::new_from_array(spl_token_v2_0::native_mint::id().to_bytes())
24}
25
26pub fn spl_token_v2_0_pubkey(pubkey: &Pubkey) -> SplTokenPubkey {
28 SplTokenPubkey::new_from_array(pubkey.to_bytes())
29}
30
31pub fn pubkey_from_spl_token_v2_0(pubkey: &SplTokenPubkey) -> Pubkey {
33 Pubkey::new_from_array(pubkey.to_bytes())
34}
35
36pub fn parse_token(
37 data: &[u8],
38 mint_decimals: Option<u8>,
39) -> Result<TokenAccountType, ParseAccountError> {
40 if data.len() == Account::get_packed_len() {
41 let account = Account::unpack(data)
42 .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
43 let decimals = mint_decimals.ok_or_else(|| {
44 ParseAccountError::AdditionalDataMissing(
45 "no mint_decimals provided to parse spl-token account".to_string(),
46 )
47 })?;
48 Ok(TokenAccountType::Account(UiTokenAccount {
49 mint: account.mint.to_string(),
50 owner: account.owner.to_string(),
51 token_amount: token_amount_to_ui_amount(account.amount, decimals),
52 delegate: match account.delegate {
53 COption::Some(pubkey) => Some(pubkey.to_string()),
54 COption::None => None,
55 },
56 state: account.state.into(),
57 is_native: account.is_native(),
58 rent_exempt_reserve: match account.is_native {
59 COption::Some(reserve) => Some(token_amount_to_ui_amount(reserve, decimals)),
60 COption::None => None,
61 },
62 delegated_amount: if account.delegate.is_none() {
63 None
64 } else {
65 Some(token_amount_to_ui_amount(
66 account.delegated_amount,
67 decimals,
68 ))
69 },
70 close_authority: match account.close_authority {
71 COption::Some(pubkey) => Some(pubkey.to_string()),
72 COption::None => None,
73 },
74 }))
75 } else if data.len() == Mint::get_packed_len() {
76 let mint = Mint::unpack(data)
77 .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
78 Ok(TokenAccountType::Mint(UiMint {
79 mint_authority: match mint.mint_authority {
80 COption::Some(pubkey) => Some(pubkey.to_string()),
81 COption::None => None,
82 },
83 supply: mint.supply.to_string(),
84 decimals: mint.decimals,
85 is_initialized: mint.is_initialized,
86 freeze_authority: match mint.freeze_authority {
87 COption::Some(pubkey) => Some(pubkey.to_string()),
88 COption::None => None,
89 },
90 }))
91 } else if data.len() == Multisig::get_packed_len() {
92 let multisig = Multisig::unpack(data)
93 .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?;
94 Ok(TokenAccountType::Multisig(UiMultisig {
95 num_required_signers: multisig.m,
96 num_valid_signers: multisig.n,
97 is_initialized: multisig.is_initialized,
98 signers: multisig
99 .signers
100 .iter()
101 .filter_map(|pubkey| {
102 if pubkey != &SplTokenPubkey::default() {
103 Some(pubkey.to_string())
104 } else {
105 None
106 }
107 })
108 .collect(),
109 }))
110 } else {
111 Err(ParseAccountError::AccountNotParsable(
112 ParsableAccount::SplToken,
113 ))
114 }
115}
116
117#[derive(Debug, Serialize, Deserialize, PartialEq)]
118#[serde(rename_all = "camelCase", tag = "type", content = "info")]
119pub enum TokenAccountType {
120 Account(UiTokenAccount),
121 Mint(UiMint),
122 Multisig(UiMultisig),
123}
124
125#[derive(Debug, Serialize, Deserialize, PartialEq)]
126#[serde(rename_all = "camelCase")]
127pub struct UiTokenAccount {
128 pub mint: String,
129 pub owner: String,
130 pub token_amount: UiTokenAmount,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub delegate: Option<String>,
133 pub state: UiAccountState,
134 pub is_native: bool,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub rent_exempt_reserve: Option<UiTokenAmount>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub delegated_amount: Option<UiTokenAmount>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub close_authority: Option<String>,
141}
142
143#[derive(Debug, Serialize, Deserialize, PartialEq)]
144#[serde(rename_all = "camelCase")]
145pub enum UiAccountState {
146 Uninitialized,
147 Initialized,
148 Frozen,
149}
150
151impl From<AccountState> for UiAccountState {
152 fn from(state: AccountState) -> Self {
153 match state {
154 AccountState::Uninitialized => UiAccountState::Uninitialized,
155 AccountState::Initialized => UiAccountState::Initialized,
156 AccountState::Frozen => UiAccountState::Frozen,
157 }
158 }
159}
160
161pub fn real_number_string(amount: u64, decimals: u8) -> StringDecimals {
162 let decimals = decimals as usize;
163 if decimals > 0 {
164 let mut s = format!("{:01$}", amount, decimals + 1);
166 s.insert(s.len() - decimals, '.');
168 s
169 } else {
170 amount.to_string()
171 }
172}
173
174pub fn real_number_string_trimmed(amount: u64, decimals: u8) -> StringDecimals {
175 let mut s = real_number_string(amount, decimals);
176 if decimals > 0 {
177 let zeros_trimmed = s.trim_end_matches('0');
178 s = zeros_trimmed.trim_end_matches('.').to_string();
179 }
180 s
181}
182
183#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
184#[serde(rename_all = "camelCase")]
185pub struct UiTokenAmount {
186 pub ui_amount: Option<f64>,
187 pub decimals: u8,
188 pub amount: StringAmount,
189 pub ui_amount_string: StringDecimals,
190}
191
192impl UiTokenAmount {
193 pub fn real_number_string(&self) -> String {
194 real_number_string(
195 u64::from_str(&self.amount).unwrap_or_default(),
196 self.decimals as u8,
197 )
198 }
199
200 pub fn real_number_string_trimmed(&self) -> String {
201 if !self.ui_amount_string.is_empty() {
202 self.ui_amount_string.clone()
203 } else {
204 real_number_string_trimmed(
205 u64::from_str(&self.amount).unwrap_or_default(),
206 self.decimals as u8,
207 )
208 }
209 }
210}
211
212pub fn token_amount_to_ui_amount(amount: u64, decimals: u8) -> UiTokenAmount {
213 let amount_decimals = 10_usize
214 .checked_pow(decimals as u32)
215 .map(|dividend| amount as f64 / dividend as f64);
216 UiTokenAmount {
217 ui_amount: amount_decimals,
218 decimals,
219 amount: amount.to_string(),
220 ui_amount_string: real_number_string_trimmed(amount, decimals),
221 }
222}
223
224#[derive(Debug, Serialize, Deserialize, PartialEq)]
225#[serde(rename_all = "camelCase")]
226pub struct UiMint {
227 pub mint_authority: Option<String>,
228 pub supply: StringAmount,
229 pub decimals: u8,
230 pub is_initialized: bool,
231 pub freeze_authority: Option<String>,
232}
233
234#[derive(Debug, Serialize, Deserialize, PartialEq)]
235#[serde(rename_all = "camelCase")]
236pub struct UiMultisig {
237 pub num_required_signers: u8,
238 pub num_valid_signers: u8,
239 pub is_initialized: bool,
240 pub signers: Vec<String>,
241}
242
243pub fn get_token_account_mint(data: &[u8]) -> Option<Pubkey> {
244 if data.len() == Account::get_packed_len() {
245 Some(Pubkey::new(&data[0..32]))
246 } else {
247 None
248 }
249}
250
251#[cfg(test)]
252mod test {
253 use super::*;
254
255 #[test]
256 fn test_parse_token() {
257 let mint_pubkey = SplTokenPubkey::new(&[2; 32]);
258 let owner_pubkey = SplTokenPubkey::new(&[3; 32]);
259 let mut account_data = vec![0; Account::get_packed_len()];
260 let mut account = Account::unpack_unchecked(&account_data).unwrap();
261 account.mint = mint_pubkey;
262 account.owner = owner_pubkey;
263 account.amount = 42;
264 account.state = AccountState::Initialized;
265 account.is_native = COption::None;
266 account.close_authority = COption::Some(owner_pubkey);
267 Account::pack(account, &mut account_data).unwrap();
268
269 assert!(parse_token(&account_data, None).is_err());
270 assert_eq!(
271 parse_token(&account_data, Some(2)).unwrap(),
272 TokenAccountType::Account(UiTokenAccount {
273 mint: mint_pubkey.to_string(),
274 owner: owner_pubkey.to_string(),
275 token_amount: UiTokenAmount {
276 ui_amount: Some(0.42),
277 decimals: 2,
278 amount: "42".to_string(),
279 ui_amount_string: "0.42".to_string()
280 },
281 delegate: None,
282 state: UiAccountState::Initialized,
283 is_native: false,
284 rent_exempt_reserve: None,
285 delegated_amount: None,
286 close_authority: Some(owner_pubkey.to_string()),
287 }),
288 );
289
290 let mut mint_data = vec![0; Mint::get_packed_len()];
291 let mut mint = Mint::unpack_unchecked(&mint_data).unwrap();
292 mint.mint_authority = COption::Some(owner_pubkey);
293 mint.supply = 42;
294 mint.decimals = 3;
295 mint.is_initialized = true;
296 mint.freeze_authority = COption::Some(owner_pubkey);
297 Mint::pack(mint, &mut mint_data).unwrap();
298
299 assert_eq!(
300 parse_token(&mint_data, None).unwrap(),
301 TokenAccountType::Mint(UiMint {
302 mint_authority: Some(owner_pubkey.to_string()),
303 supply: 42.to_string(),
304 decimals: 3,
305 is_initialized: true,
306 freeze_authority: Some(owner_pubkey.to_string()),
307 }),
308 );
309
310 let signer1 = SplTokenPubkey::new(&[1; 32]);
311 let signer2 = SplTokenPubkey::new(&[2; 32]);
312 let signer3 = SplTokenPubkey::new(&[3; 32]);
313 let mut multisig_data = vec![0; Multisig::get_packed_len()];
314 let mut signers = [SplTokenPubkey::default(); 11];
315 signers[0] = signer1;
316 signers[1] = signer2;
317 signers[2] = signer3;
318 let mut multisig = Multisig::unpack_unchecked(&multisig_data).unwrap();
319 multisig.m = 2;
320 multisig.n = 3;
321 multisig.is_initialized = true;
322 multisig.signers = signers;
323 Multisig::pack(multisig, &mut multisig_data).unwrap();
324
325 assert_eq!(
326 parse_token(&multisig_data, None).unwrap(),
327 TokenAccountType::Multisig(UiMultisig {
328 num_required_signers: 2,
329 num_valid_signers: 3,
330 is_initialized: true,
331 signers: vec![
332 signer1.to_string(),
333 signer2.to_string(),
334 signer3.to_string()
335 ],
336 }),
337 );
338
339 let bad_data = vec![0; 4];
340 assert!(parse_token(&bad_data, None).is_err());
341 }
342
343 #[test]
344 fn test_get_token_account_mint() {
345 let mint_pubkey = SplTokenPubkey::new(&[2; 32]);
346 let mut account_data = vec![0; Account::get_packed_len()];
347 let mut account = Account::unpack_unchecked(&account_data).unwrap();
348 account.mint = mint_pubkey;
349 Account::pack(account, &mut account_data).unwrap();
350
351 let expected_mint_pubkey = Pubkey::new(&[2; 32]);
352 assert_eq!(
353 get_token_account_mint(&account_data),
354 Some(expected_mint_pubkey)
355 );
356 }
357
358 #[test]
359 fn test_ui_token_amount_real_string() {
360 assert_eq!(&real_number_string(1, 0), "1");
361 assert_eq!(&real_number_string_trimmed(1, 0), "1");
362 let token_amount = token_amount_to_ui_amount(1, 0);
363 assert_eq!(
364 token_amount.ui_amount_string,
365 real_number_string_trimmed(1, 0)
366 );
367 assert_eq!(token_amount.ui_amount, Some(1.0));
368 assert_eq!(&real_number_string(10, 0), "10");
369 assert_eq!(&real_number_string_trimmed(10, 0), "10");
370 let token_amount = token_amount_to_ui_amount(10, 0);
371 assert_eq!(
372 token_amount.ui_amount_string,
373 real_number_string_trimmed(10, 0)
374 );
375 assert_eq!(token_amount.ui_amount, Some(10.0));
376 assert_eq!(&real_number_string(1, 9), "0.000000001");
377 assert_eq!(&real_number_string_trimmed(1, 9), "0.000000001");
378 let token_amount = token_amount_to_ui_amount(1, 9);
379 assert_eq!(
380 token_amount.ui_amount_string,
381 real_number_string_trimmed(1, 9)
382 );
383 assert_eq!(token_amount.ui_amount, Some(0.000000001));
384 assert_eq!(&real_number_string(1_000_000_000, 9), "1.000000000");
385 assert_eq!(&real_number_string_trimmed(1_000_000_000, 9), "1");
386 let token_amount = token_amount_to_ui_amount(1_000_000_000, 9);
387 assert_eq!(
388 token_amount.ui_amount_string,
389 real_number_string_trimmed(1_000_000_000, 9)
390 );
391 assert_eq!(token_amount.ui_amount, Some(1.0));
392 assert_eq!(&real_number_string(1_234_567_890, 3), "1234567.890");
393 assert_eq!(&real_number_string_trimmed(1_234_567_890, 3), "1234567.89");
394 let token_amount = token_amount_to_ui_amount(1_234_567_890, 3);
395 assert_eq!(
396 token_amount.ui_amount_string,
397 real_number_string_trimmed(1_234_567_890, 3)
398 );
399 assert_eq!(token_amount.ui_amount, Some(1234567.89));
400 assert_eq!(
401 &real_number_string(1_234_567_890, 25),
402 "0.0000000000000001234567890"
403 );
404 assert_eq!(
405 &real_number_string_trimmed(1_234_567_890, 25),
406 "0.000000000000000123456789"
407 );
408 let token_amount = token_amount_to_ui_amount(1_234_567_890, 20);
409 assert_eq!(
410 token_amount.ui_amount_string,
411 real_number_string_trimmed(1_234_567_890, 20)
412 );
413 assert_eq!(token_amount.ui_amount, None);
414 }
415
416 #[test]
417 fn test_ui_token_amount_real_string_zero() {
418 assert_eq!(&real_number_string(0, 0), "0");
419 assert_eq!(&real_number_string_trimmed(0, 0), "0");
420 let token_amount = token_amount_to_ui_amount(0, 0);
421 assert_eq!(
422 token_amount.ui_amount_string,
423 real_number_string_trimmed(0, 0)
424 );
425 assert_eq!(token_amount.ui_amount, Some(0.0));
426 assert_eq!(&real_number_string(0, 9), "0.000000000");
427 assert_eq!(&real_number_string_trimmed(0, 9), "0");
428 let token_amount = token_amount_to_ui_amount(0, 9);
429 assert_eq!(
430 token_amount.ui_amount_string,
431 real_number_string_trimmed(0, 9)
432 );
433 assert_eq!(token_amount.ui_amount, Some(0.0));
434 assert_eq!(&real_number_string(0, 25), "0.0000000000000000000000000");
435 assert_eq!(&real_number_string_trimmed(0, 25), "0");
436 let token_amount = token_amount_to_ui_amount(0, 20);
437 assert_eq!(
438 token_amount.ui_amount_string,
439 real_number_string_trimmed(0, 20)
440 );
441 assert_eq!(token_amount.ui_amount, None);
442 }
443}