gemachain_account_decoder/
parse_token.rs

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
14// A helper function to convert spl_token_v2_0::id() as spl_sdk::pubkey::Pubkey to
15// gemachain_sdk::pubkey::Pubkey
16pub fn spl_token_id_v2_0() -> Pubkey {
17    Pubkey::new_from_array(spl_token_v2_0::id().to_bytes())
18}
19
20// A helper function to convert spl_token_v2_0::native_mint::id() as spl_sdk::pubkey::Pubkey to
21// gemachain_sdk::pubkey::Pubkey
22pub fn spl_token_v2_0_native_mint() -> Pubkey {
23    Pubkey::new_from_array(spl_token_v2_0::native_mint::id().to_bytes())
24}
25
26// A helper function to convert a gemachain_sdk::pubkey::Pubkey to spl_sdk::pubkey::Pubkey
27pub fn spl_token_v2_0_pubkey(pubkey: &Pubkey) -> SplTokenPubkey {
28    SplTokenPubkey::new_from_array(pubkey.to_bytes())
29}
30
31// A helper function to convert a spl_sdk::pubkey::Pubkey to gemachain_sdk::pubkey::Pubkey
32pub 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        // Left-pad zeros to decimals + 1, so we at least have an integer zero
165        let mut s = format!("{:01$}", amount, decimals + 1);
166        // Add the decimal point (Sorry, "," locales!)
167        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}