Skip to main content

light_token_interface/state/token/
token_struct.rs

1use light_compressed_account::Pubkey;
2use light_zero_copy::errors::ZeroCopyError;
3
4use crate::{state::ExtensionStruct, AnchorDeserialize, AnchorSerialize, TokenError};
5
6/// AccountType discriminator value for token accounts (at byte 165)
7pub const ACCOUNT_TYPE_TOKEN_ACCOUNT: u8 = 2;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, AnchorSerialize, AnchorDeserialize)]
10#[repr(u8)]
11pub enum AccountState {
12    Uninitialized = 0,
13    Initialized = 1,
14    Frozen = 2,
15}
16
17impl TryFrom<u8> for AccountState {
18    type Error = TokenError;
19
20    fn try_from(value: u8) -> Result<Self, Self::Error> {
21        match value {
22            0 => Ok(AccountState::Uninitialized),
23            1 => Ok(AccountState::Initialized),
24            2 => Ok(AccountState::Frozen),
25            _ => Err(TokenError::InvalidAccountState),
26        }
27    }
28}
29
30/// Ctoken account structure (same as SPL Token Account but with extensions).
31/// Ctokens are solana accounts, compressed tokens are stored
32/// as TokenData that is optimized for compressed accounts.
33#[derive(Debug, PartialEq, Eq, Hash, Clone)]
34pub struct Token {
35    /// The mint associated with this account
36    pub mint: Pubkey,
37    /// The owner of this account.
38    pub owner: Pubkey,
39    /// The amount of tokens this account holds.
40    pub amount: u64,
41    /// If `delegate` is `Some` then `delegated_amount` represents
42    /// the amount authorized by the delegate
43    pub delegate: Option<Pubkey>,
44    /// The account's state
45    pub state: AccountState,
46    /// If `is_some`, this is a native token, and the value logs the rent-exempt
47    /// reserve. An Account is required to be rent-exempt, so the value is
48    /// used by the Processor to ensure that wrapped SOL accounts do not
49    /// drop below this threshold.
50    pub is_native: Option<u64>,
51    /// The amount delegated
52    pub delegated_amount: u64,
53    /// Optional authority to close the account.
54    pub close_authority: Option<Pubkey>,
55    /// Account type discriminator (at byte 165 when extensions present).
56    /// For valid Token accounts this is ACCOUNT_TYPE_TOKEN_ACCOUNT (2).
57    pub account_type: u8,
58    /// Extensions for the token account (including compressible config)
59    pub extensions: Option<Vec<ExtensionStruct>>,
60}
61
62// IdlBuild trait impl (provides default implementations)
63#[cfg(feature = "idl-build")]
64impl anchor_lang::IdlBuild for Token {}
65
66// IDL inherent methods required for UFCS calls from AnchorSerialize derive macro.
67// When anchor-lang/idl-build is enabled, the macro generates code like
68// `<Token>::get_full_path()`. These calls need inherent methods since the
69// IdlBuild trait may not be in scope at the call site.
70#[cfg(feature = "idl-build")]
71impl Token {
72    #[doc(hidden)]
73    pub fn get_full_path() -> String {
74        std::any::type_name::<Self>().into()
75    }
76
77    #[doc(hidden)]
78    pub fn create_type() -> Option<anchor_lang::idl::types::IdlTypeDef> {
79        None
80    }
81
82    #[doc(hidden)]
83    pub fn insert_types(
84        _types: &mut std::collections::BTreeMap<String, anchor_lang::idl::types::IdlTypeDef>,
85    ) {
86    }
87}
88
89impl Token {
90    /// Extract amount directly from account data slice using hardcoded offset
91    /// Token layout: mint (32 bytes) + owner (32 bytes) + amount (8 bytes)
92    pub fn amount_from_slice(data: &[u8]) -> Result<u64, ZeroCopyError> {
93        const AMOUNT_OFFSET: usize = 64; // 32 (mint) + 32 (owner)
94
95        check_token_account(data)?;
96
97        #[inline(always)]
98        fn check_token_account(bytes: &[u8]) -> Result<(), ZeroCopyError> {
99            if bytes.len() == 165 || (bytes.len() > 165 && bytes[165] == ACCOUNT_TYPE_TOKEN_ACCOUNT)
100            {
101                Ok(())
102            } else {
103                Err(ZeroCopyError::InvalidConversion)
104            }
105        }
106
107        let amount_bytes = &data[AMOUNT_OFFSET..AMOUNT_OFFSET + 8];
108        let amount = u64::from_le_bytes(amount_bytes.try_into().map_err(|_| ZeroCopyError::Size)?);
109
110        Ok(amount)
111    }
112
113    /// Extract amount from an AccountInfo
114    #[cfg(feature = "solana")]
115    pub fn amount_from_account_info(
116        account_info: &solana_account_info::AccountInfo,
117    ) -> Result<u64, ZeroCopyError> {
118        let data = account_info
119            .try_borrow_data()
120            .map_err(|_| ZeroCopyError::Size)?;
121        Self::amount_from_slice(&data)
122    }
123
124    /// Checks if account is frozen
125    pub fn is_frozen(&self) -> bool {
126        self.state == AccountState::Frozen
127    }
128
129    /// Checks if account is native
130    pub fn is_native(&self) -> bool {
131        self.is_native.is_some()
132    }
133
134    /// Checks if account is initialized
135    pub fn is_initialized(&self) -> bool {
136        self.state == AccountState::Initialized
137    }
138
139    /// Returns the account type discriminator
140    #[inline(always)]
141    pub fn account_type(&self) -> u8 {
142        self.account_type
143    }
144
145    /// Checks if account_type matches Token discriminator value
146    #[inline(always)]
147    pub fn is_token_account(&self) -> bool {
148        self.account_type == ACCOUNT_TYPE_TOKEN_ACCOUNT
149    }
150}