kora_lib/token/
token22.rs

1use super::interface::{TokenInterface, TokenState};
2use async_trait::async_trait;
3use solana_program::{program_pack::Pack, pubkey::Pubkey};
4use solana_sdk::instruction::Instruction;
5use spl_associated_token_account::{
6    get_associated_token_address_with_program_id, instruction::create_associated_token_account,
7};
8use spl_token_2022::{
9    extension::{
10        cpi_guard::CpiGuard, interest_bearing_mint::InterestBearingConfig,
11        transfer_fee::TransferFeeConfig, BaseStateWithExtensions, ExtensionType,
12        StateWithExtensions,
13    },
14    instruction,
15    state::{Account as Token2022AccountState, AccountState, Mint as Token2022MintState},
16};
17use std::fmt::Debug;
18/// To access extension data, use the has_extension and get_* methods provided by this struct.
19/// Supported extensions:
20/// - TransferFee (fees applied on transfers)
21/// - ConfidentialTransfer (private transfers)
22/// - NonTransferable (tokens that cannot be transferred)
23/// - InterestBearing (interest accruing tokens)
24/// - CpiGuard (restrict cross-program invocations)
25/// - MemoTransfer (require memo on transfers)
26/// - DefaultAccountState (default frozen state)
27/// - ImmutableOwner (cannot change account owner)
28/// - PermanentDelegate (permanent authority)
29/// - TokenMetadata (on-chain metadata)
30/// - TransferHook (custom hooks on transfer)
31/// - GroupPointer/GroupMemberPointer (token grouping)
32/// - ConfidentialTransferFee (private transfers with fees)
33/// - MetadataPointer (pointer to off-chain metadata)
34/// - MintCloseAuthority (authority to close mint)
35/// - Pausable (ability to pause transfers)
36/// - ScaledUiAmount (custom UI amount scaling)
37#[derive(Debug)]
38pub struct Token2022Account {
39    pub mint: Pubkey,
40    pub owner: Pubkey,
41    pub amount: u64,
42    pub delegate: Option<Pubkey>,
43    pub state: u8,
44    pub is_native: Option<u64>,
45    pub delegated_amount: u64,
46    pub close_authority: Option<Pubkey>,
47    /// Raw extension data for accessing all Token-2022 extensions
48    pub extension_data: Vec<u8>,
49}
50
51impl TokenState for Token2022Account {
52    fn mint(&self) -> Pubkey {
53        self.mint
54    }
55    fn owner(&self) -> Pubkey {
56        self.owner
57    }
58    fn amount(&self) -> u64 {
59        self.amount
60    }
61    fn decimals(&self) -> u8 {
62        0
63    }
64    fn as_any(&self) -> &dyn std::any::Any {
65        self
66    }
67}
68
69impl Token2022Account {
70    /// Check if account has a specific extension type
71    pub fn has_extension(&self, extension_type: ExtensionType) -> bool {
72        if let Ok(account_with_extensions) =
73            StateWithExtensions::<Token2022AccountState>::unpack(&self.extension_data)
74        {
75            account_with_extensions
76                .get_extension_types()
77                .unwrap_or_default()
78                .contains(&extension_type)
79        } else {
80            false
81        }
82    }
83
84    /// Get transfer fee configuration if present
85    pub fn get_transfer_fee(&self) -> Option<TransferFeeConfig> {
86        if let Ok(account_with_extensions) =
87            StateWithExtensions::<Token2022AccountState>::unpack(&self.extension_data)
88        {
89            account_with_extensions.get_extension::<TransferFeeConfig>().ok().copied()
90        } else {
91            None
92        }
93    }
94
95    /// Check if token is non-transferable
96    pub fn is_non_transferable(&self) -> bool {
97        self.has_extension(ExtensionType::NonTransferable)
98    }
99
100    /// Get interest bearing configuration if present
101    pub fn get_interest_config(&self) -> Option<InterestBearingConfig> {
102        if let Ok(account_with_extensions) =
103            StateWithExtensions::<Token2022AccountState>::unpack(&self.extension_data)
104        {
105            account_with_extensions.get_extension::<InterestBearingConfig>().ok().copied()
106        } else {
107            None
108        }
109    }
110
111    /// Check if CPI guard is enabled
112    pub fn is_cpi_guarded(&self) -> bool {
113        if let Ok(account_with_extensions) =
114            StateWithExtensions::<Token2022AccountState>::unpack(&self.extension_data)
115        {
116            if let Ok(cpi_guard) = account_with_extensions.get_extension::<CpiGuard>() {
117                return cpi_guard.lock_cpi.into();
118            }
119        }
120        false
121    }
122
123    /// Check if confidential transfers are enabled
124    pub fn has_confidential_transfers(&self) -> bool {
125        self.has_extension(ExtensionType::ConfidentialTransferAccount)
126    }
127}
128
129pub struct Token2022Program;
130
131impl Default for Token2022Program {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137impl Token2022Program {
138    pub fn new() -> Self {
139        Self
140    }
141
142    fn get_program_id(&self) -> Pubkey {
143        spl_token_2022::id()
144    }
145}
146
147#[async_trait]
148impl TokenInterface for Token2022Program {
149    fn program_id(&self) -> Pubkey {
150        self.get_program_id()
151    }
152
153    fn unpack_token_account(
154        &self,
155        data: &[u8],
156    ) -> Result<Box<dyn TokenState + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
157        let account = StateWithExtensions::<Token2022AccountState>::unpack(data)?;
158        let base = account.base;
159
160        Ok(Box::new(Token2022Account {
161            mint: base.mint,
162            owner: base.owner,
163            amount: base.amount,
164            delegate: base.delegate.into(),
165            state: match base.state {
166                AccountState::Uninitialized => 0,
167                AccountState::Initialized => 1,
168                AccountState::Frozen => 2,
169            },
170            is_native: base.is_native.into(),
171            delegated_amount: base.delegated_amount,
172            close_authority: base.close_authority.into(),
173            extension_data: data.to_vec(),
174        }))
175    }
176
177    fn create_initialize_account_instruction(
178        &self,
179        account: &Pubkey,
180        mint: &Pubkey,
181        owner: &Pubkey,
182    ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
183        Ok(spl_token_2022::instruction::initialize_account3(
184            &self.program_id(),
185            account,
186            mint,
187            owner,
188        )?)
189    }
190
191    fn create_transfer_instruction(
192        &self,
193        source: &Pubkey,
194        destination: &Pubkey,
195        authority: &Pubkey,
196        amount: u64,
197    ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
198        // Get the mint from the source account data
199        Ok(spl_token_2022::instruction::transfer(
200            &self.program_id(),
201            source,
202            destination,
203            authority,
204            &[],
205            amount,
206        )?)
207    }
208
209    fn create_transfer_checked_instruction(
210        &self,
211        source: &Pubkey,
212        mint: &Pubkey,
213        destination: &Pubkey,
214        authority: &Pubkey,
215        amount: u64,
216        decimals: u8,
217    ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
218        Ok(spl_token_2022::instruction::transfer_checked(
219            &self.program_id(),
220            source,
221            mint,
222            destination,
223            authority,
224            &[],
225            amount,
226            decimals,
227        )?)
228    }
229
230    fn get_associated_token_address(&self, wallet: &Pubkey, mint: &Pubkey) -> Pubkey {
231        get_associated_token_address_with_program_id(wallet, mint, &self.program_id())
232    }
233
234    fn create_associated_token_account_instruction(
235        &self,
236        funding_account: &Pubkey,
237        wallet: &Pubkey,
238        mint: &Pubkey,
239    ) -> Instruction {
240        create_associated_token_account(funding_account, wallet, mint, &self.program_id())
241    }
242
243    fn get_mint_decimals(
244        &self,
245        mint_data: &[u8],
246    ) -> Result<u8, Box<dyn std::error::Error + Send + Sync>> {
247        let mint = StateWithExtensions::<Token2022MintState>::unpack(mint_data)?;
248        Ok(mint.base.decimals)
249    }
250
251    fn decode_transfer_instruction(
252        &self,
253        data: &[u8],
254    ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
255        let instruction = instruction::TokenInstruction::unpack(data)?;
256        match instruction {
257            instruction::TokenInstruction::Transfer { amount } => Ok(amount),
258            instruction::TokenInstruction::TransferChecked { amount, .. } => Ok(amount),
259            _ => Err("Not a transfer instruction".into()),
260        }
261    }
262}