Skip to main content

kora_lib/token/
spl_token.rs

1use crate::token::interface::TokenMint;
2
3use super::interface::{TokenInterface, TokenState};
4use async_trait::async_trait;
5use solana_program::pubkey::Pubkey;
6use solana_sdk::{instruction::Instruction, program_pack::Pack};
7use spl_associated_token_account_interface::{
8    address::get_associated_token_address_with_program_id,
9    instruction::create_associated_token_account,
10};
11use spl_token_interface::{
12    self,
13    state::{Account as TokenAccountState, AccountState, Mint as MintState},
14};
15
16#[derive(Debug)]
17pub struct TokenAccount {
18    pub mint: Pubkey,
19    pub owner: Pubkey,
20    pub amount: u64,
21    pub delegate: Option<Pubkey>,
22    pub state: u8,
23    pub is_native: Option<u64>,
24    pub delegated_amount: u64,
25    pub close_authority: Option<Pubkey>,
26}
27
28impl TokenState for TokenAccount {
29    fn mint(&self) -> Pubkey {
30        self.mint
31    }
32    fn owner(&self) -> Pubkey {
33        self.owner
34    }
35    fn amount(&self) -> u64 {
36        self.amount
37    }
38    fn decimals(&self) -> u8 {
39        0
40    }
41    fn as_any(&self) -> &dyn std::any::Any {
42        self
43    }
44}
45
46#[derive(Debug)]
47pub struct SplMint {
48    pub mint: Pubkey,
49    pub mint_authority: Option<Pubkey>,
50    pub supply: u64,
51    pub decimals: u8,
52    pub is_initialized: bool,
53    pub freeze_authority: Option<Pubkey>,
54}
55
56impl TokenMint for SplMint {
57    fn address(&self) -> Pubkey {
58        self.mint
59    }
60
61    fn decimals(&self) -> u8 {
62        self.decimals
63    }
64
65    fn mint_authority(&self) -> Option<Pubkey> {
66        self.mint_authority
67    }
68
69    fn supply(&self) -> u64 {
70        self.supply
71    }
72
73    fn freeze_authority(&self) -> Option<Pubkey> {
74        self.freeze_authority
75    }
76
77    fn is_initialized(&self) -> bool {
78        self.is_initialized
79    }
80
81    fn get_token_program(&self) -> Box<dyn TokenInterface> {
82        Box::new(TokenProgram::new())
83    }
84
85    fn as_any(&self) -> &dyn std::any::Any {
86        self
87    }
88}
89
90pub struct TokenProgram;
91
92impl Default for TokenProgram {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl TokenProgram {
99    pub fn new() -> Self {
100        Self
101    }
102}
103
104#[async_trait]
105impl TokenInterface for TokenProgram {
106    fn program_id(&self) -> Pubkey {
107        spl_token_interface::id()
108    }
109
110    fn unpack_token_account(
111        &self,
112        data: &[u8],
113    ) -> Result<Box<dyn TokenState + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
114        let account = TokenAccountState::unpack(data)?;
115
116        Ok(Box::new(TokenAccount {
117            mint: account.mint,
118            owner: account.owner,
119            amount: account.amount,
120            delegate: account.delegate.into(),
121            state: match account.state {
122                AccountState::Uninitialized => 0,
123                AccountState::Initialized => 1,
124                AccountState::Frozen => 2,
125            },
126            is_native: account.is_native.into(),
127            delegated_amount: account.delegated_amount,
128            close_authority: account.close_authority.into(),
129        }))
130    }
131
132    fn create_initialize_account_instruction(
133        &self,
134        account: &Pubkey,
135        mint: &Pubkey,
136        owner: &Pubkey,
137    ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
138        Ok(spl_token_interface::instruction::initialize_account(
139            &self.program_id(),
140            account,
141            mint,
142            owner,
143        )?)
144    }
145
146    fn create_transfer_instruction(
147        &self,
148        source: &Pubkey,
149        destination: &Pubkey,
150        authority: &Pubkey,
151        amount: u64,
152    ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
153        Ok(spl_token_interface::instruction::transfer(
154            &self.program_id(),
155            source,
156            destination,
157            authority,
158            &[],
159            amount,
160        )?)
161    }
162
163    fn create_transfer_checked_instruction(
164        &self,
165        source: &Pubkey,
166        mint: &Pubkey,
167        destination: &Pubkey,
168        authority: &Pubkey,
169        amount: u64,
170        decimals: u8,
171    ) -> Result<Instruction, Box<dyn std::error::Error + Send + Sync>> {
172        Ok(spl_token_interface::instruction::transfer_checked(
173            &self.program_id(),
174            source,
175            mint,
176            destination,
177            authority,
178            &[],
179            amount,
180            decimals,
181        )?)
182    }
183
184    fn get_associated_token_address(&self, wallet: &Pubkey, mint: &Pubkey) -> Pubkey {
185        get_associated_token_address_with_program_id(wallet, mint, &self.program_id())
186    }
187
188    fn create_associated_token_account_instruction(
189        &self,
190        funding_account: &Pubkey,
191        wallet: &Pubkey,
192        mint: &Pubkey,
193    ) -> Instruction {
194        create_associated_token_account(funding_account, wallet, mint, &self.program_id())
195    }
196
197    fn unpack_mint(
198        &self,
199        mint: &Pubkey,
200        mint_data: &[u8],
201    ) -> Result<Box<dyn TokenMint + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
202        let mint_state = MintState::unpack(mint_data)?;
203
204        Ok(Box::new(SplMint {
205            mint: *mint,
206            mint_authority: mint_state.mint_authority.into(),
207            supply: mint_state.supply,
208            decimals: mint_state.decimals,
209            is_initialized: mint_state.is_initialized,
210            freeze_authority: mint_state.freeze_authority.into(),
211        }))
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use crate::tests::common::{MintAccountMockBuilder, TokenAccountMockBuilder};
218
219    use super::*;
220    use solana_program::program_pack::Pack;
221    use solana_sdk::pubkey::Pubkey;
222    use spl_token_interface::state::{Account as SplTokenAccount, AccountState};
223
224    #[test]
225    fn test_token_program_creation_and_program_id() {
226        let program = TokenProgram::new();
227        assert_eq!(program.program_id(), spl_token_interface::id());
228    }
229
230    #[test]
231    fn test_unpack_token_account_success() {
232        let mint = Pubkey::new_unique();
233        let owner = Pubkey::new_unique();
234        let delegate = Pubkey::new_unique();
235        let close_authority = Pubkey::new_unique();
236        let amount = 1000000;
237        let delegated_amount = 500000;
238        let is_native = Some(2039280u64);
239
240        let account = TokenAccountMockBuilder::new()
241            .with_mint(&mint)
242            .with_owner(&owner)
243            .with_amount(amount)
244            .with_state(AccountState::Initialized)
245            .with_delegate(Some(delegate))
246            .with_native(is_native)
247            .with_delegated_amount(delegated_amount)
248            .with_close_authority(Some(close_authority))
249            .build();
250
251        let program = TokenProgram::new();
252        let result = program.unpack_token_account(&account.data);
253        assert!(result.is_ok());
254
255        let token_state = result.unwrap();
256        let token_account = token_state.as_any().downcast_ref::<TokenAccount>().unwrap();
257
258        assert_eq!(token_account.mint, mint);
259        assert_eq!(token_account.owner, owner);
260        assert_eq!(token_account.amount, amount);
261        assert_eq!(token_account.delegate, Some(delegate));
262        assert_eq!(token_account.state, 1); // AccountState::Initialized = 1
263        assert_eq!(token_account.is_native, is_native);
264        assert_eq!(token_account.delegated_amount, delegated_amount);
265        assert_eq!(token_account.close_authority, Some(close_authority));
266    }
267
268    #[test]
269    fn test_unpack_token_account_invalid_data() {
270        let program = TokenProgram::new();
271
272        // Test with empty data
273        let result = program.unpack_token_account(&[]);
274        assert!(result.is_err());
275
276        // Test with insufficient data
277        let short_data = vec![0u8; 10];
278        let result = program.unpack_token_account(&short_data);
279        assert!(result.is_err());
280
281        // Test with corrupted data
282        let mut corrupted_data = vec![0xFFu8; SplTokenAccount::LEN];
283        corrupted_data[0] = 0xFF; // Invalid mint pubkey start
284        let result = program.unpack_token_account(&corrupted_data);
285        assert!(result.is_err());
286    }
287
288    #[test]
289    fn test_unpack_mint_success() {
290        let mint_pubkey = Pubkey::new_unique();
291        let mint_authority = Pubkey::new_unique();
292        let freeze_authority = Pubkey::new_unique();
293        let supply = 1000000000;
294        let decimals = 6;
295
296        let account = MintAccountMockBuilder::new()
297            .with_mint_authority(Some(mint_authority))
298            .with_supply(supply)
299            .with_decimals(decimals)
300            .with_initialized(true)
301            .with_freeze_authority(Some(freeze_authority))
302            .build();
303
304        let program = TokenProgram::new();
305        let result = program.unpack_mint(&mint_pubkey, &account.data);
306        assert!(result.is_ok());
307
308        let token_mint = result.unwrap();
309        let spl_mint = token_mint.as_any().downcast_ref::<SplMint>().unwrap();
310
311        assert_eq!(spl_mint.mint, mint_pubkey);
312        assert_eq!(spl_mint.mint_authority, Some(mint_authority));
313        assert_eq!(spl_mint.supply, supply);
314        assert_eq!(spl_mint.decimals, decimals);
315        assert!(spl_mint.is_initialized);
316        assert_eq!(spl_mint.freeze_authority, Some(freeze_authority));
317    }
318
319    #[test]
320    fn test_unpack_mint_with_none_authorities() {
321        let mint_pubkey = Pubkey::new_unique();
322        // Create initialized mint with None authorities (this is valid)
323        let account = MintAccountMockBuilder::new()
324            .with_mint_authority(None)
325            .with_supply(0)
326            .with_decimals(0)
327            .with_initialized(true)
328            .with_freeze_authority(None)
329            .build();
330
331        let program = TokenProgram::new();
332        let result = program.unpack_mint(&mint_pubkey, &account.data).unwrap();
333        let spl_mint = result.as_any().downcast_ref::<SplMint>().unwrap();
334
335        assert_eq!(spl_mint.mint_authority, None);
336        assert_eq!(spl_mint.freeze_authority, None);
337        assert!(spl_mint.is_initialized); // Should be initialized to be valid
338    }
339
340    #[test]
341    fn test_unpack_mint_invalid_data() {
342        let mint_pubkey = Pubkey::new_unique();
343        let program = TokenProgram::new();
344
345        // Test with empty data
346        let result = program.unpack_mint(&mint_pubkey, &[]);
347        assert!(result.is_err());
348
349        // Test with insufficient data
350        let short_data = vec![0u8; 10];
351        let result = program.unpack_mint(&mint_pubkey, &short_data);
352        assert!(result.is_err());
353    }
354
355    #[test]
356    fn test_create_initialize_account_instruction() {
357        let program = TokenProgram::new();
358        let account = Pubkey::new_unique();
359        let mint = Pubkey::new_unique();
360        let owner = Pubkey::new_unique();
361
362        let result = program.create_initialize_account_instruction(&account, &mint, &owner);
363        assert!(result.is_ok());
364
365        let instruction = result.unwrap();
366        assert_eq!(instruction.program_id, spl_token_interface::id());
367        assert_eq!(instruction.accounts.len(), 4); // account, mint, owner, rent sysvar
368    }
369
370    #[test]
371    fn test_create_transfer_instruction() {
372        let program = TokenProgram::new();
373        let source = Pubkey::new_unique();
374        let destination = Pubkey::new_unique();
375        let authority = Pubkey::new_unique();
376        let amount = 1000000;
377
378        let result = program.create_transfer_instruction(&source, &destination, &authority, amount);
379        assert!(result.is_ok());
380
381        let instruction = result.unwrap();
382        assert_eq!(instruction.program_id, spl_token_interface::id());
383        assert_eq!(instruction.accounts.len(), 3); // source, destination, authority
384    }
385
386    #[test]
387    fn test_create_transfer_checked_instruction() {
388        let program = TokenProgram::new();
389        let source = Pubkey::new_unique();
390        let mint = Pubkey::new_unique();
391        let destination = Pubkey::new_unique();
392        let authority = Pubkey::new_unique();
393        let amount = 1000000;
394        let decimals = 6;
395
396        let result = program.create_transfer_checked_instruction(
397            &source,
398            &mint,
399            &destination,
400            &authority,
401            amount,
402            decimals,
403        );
404        assert!(result.is_ok());
405
406        let instruction = result.unwrap();
407        assert_eq!(instruction.program_id, spl_token_interface::id());
408        assert_eq!(instruction.accounts.len(), 4); // source, mint, destination, authority
409    }
410
411    #[test]
412    fn test_get_associated_token_address() {
413        let program = TokenProgram::new();
414        let wallet = Pubkey::new_unique();
415        let mint = Pubkey::new_unique();
416
417        let ata = program.get_associated_token_address(&wallet, &mint);
418
419        let ata2 = get_associated_token_address_with_program_id(
420            &wallet,
421            &mint,
422            &spl_token_interface::id(),
423        );
424
425        assert_eq!(ata, ata2);
426    }
427
428    #[test]
429    fn test_create_associated_token_account_instruction() {
430        let program = TokenProgram::new();
431        let funding_account = Pubkey::new_unique();
432        let wallet = Pubkey::new_unique();
433        let mint = Pubkey::new_unique();
434
435        let instruction =
436            program.create_associated_token_account_instruction(&funding_account, &wallet, &mint);
437
438        assert_eq!(instruction.program_id, spl_associated_token_account_interface::program::id());
439        assert_eq!(instruction.accounts.len(), 6); // funding, ata, wallet, mint, system_program, token_program
440    }
441
442    #[test]
443    fn test_spl_mint_get_token_program() {
444        let spl_mint = SplMint {
445            mint: Pubkey::new_unique(),
446            mint_authority: None,
447            supply: 0,
448            decimals: 0,
449            is_initialized: false,
450            freeze_authority: None,
451        };
452
453        let token_program = spl_mint.get_token_program();
454        assert_eq!(token_program.program_id(), spl_token_interface::id());
455    }
456
457    #[test]
458    fn test_spl_mint_as_any_downcasting() {
459        let spl_mint = SplMint {
460            mint: Pubkey::new_unique(),
461            mint_authority: None,
462            supply: 1000000,
463            decimals: 6,
464            is_initialized: true,
465            freeze_authority: None,
466        };
467
468        let any_ref = spl_mint.as_any();
469        assert!(any_ref.is::<SplMint>());
470
471        let downcast_result = any_ref.downcast_ref::<SplMint>();
472        assert!(downcast_result.is_some());
473        assert_eq!(downcast_result.unwrap().supply, 1000000);
474    }
475
476    #[test]
477    fn test_spl_mint_with_none_authorities() {
478        let spl_mint = SplMint {
479            mint: Pubkey::new_unique(),
480            mint_authority: None,
481            supply: 0,
482            decimals: 0,
483            is_initialized: false,
484            freeze_authority: None,
485        };
486
487        assert_eq!(spl_mint.mint_authority(), None);
488        assert_eq!(spl_mint.freeze_authority(), None);
489        assert!(!spl_mint.is_initialized());
490    }
491}