openzeppelin_relayer/domain/relayer/solana/
token.rs

1//! Solana token programs interaction module.
2//!
3//! This module provides abstractions and utilities for interacting with Solana token programs,
4//! specifically SPL Token and Token-2022. It offers unified interfaces for common token operations
5//! like transfers, account creation, and account data parsing.
6//!
7//! This module abstracts away differences between token program versions, allowing
8//! for consistent interaction regardless of which token program (SPL Token or Token-2022)
9//! is being used.
10use ::spl_token::state::Account as SplTokenAccount;
11use log::error;
12use solana_sdk::{
13    account::Account as SolanaAccount, instruction::Instruction, program_pack::Pack, pubkey::Pubkey,
14};
15use spl_associated_token_account::get_associated_token_address_with_program_id;
16
17use spl_associated_token_account::instruction::create_associated_token_account;
18
19use crate::services::SolanaProviderTrait;
20
21/// Represents a Solana token account with its key properties.
22///
23/// This struct contains the essential information about a token account,
24/// including the mint address, owner, token amount, and frozen status.
25#[derive(Debug, Clone, Copy)]
26pub struct TokenAccount {
27    /// The mint address of the token
28    pub mint: Pubkey,
29    /// The owner of the token account
30    pub owner: Pubkey,
31    /// The amount of tokens held in this account
32    pub amount: u64,
33    /// Whether the account is frozen
34    pub is_frozen: bool,
35}
36
37/// Error types that can occur during token operations.
38///
39/// This enum provides specific error variants for different token-related failures,
40/// making it easier to diagnose and handle token operation issues.
41#[derive(Debug, thiserror::Error)]
42pub enum TokenError {
43    /// Error when a token instruction is invalid
44    #[error("Invalid token instruction: {0}")]
45    InvalidTokenInstruction(String),
46    /// Error when a token mint is invalid
47    #[error("Invalid token mint: {0}")]
48    InvalidTokenMint(String),
49    /// Error when a token program is invalid
50    #[error("Invalid token program: {0}")]
51    InvalidTokenProgram(String),
52    /// Error when an instruction fails
53    #[error("Instruction error: {0}")]
54    Instruction(String),
55    /// Error when an account operation fails
56    #[error("Account error: {0}")]
57    AccountError(String),
58}
59
60/// Represents different types of token instructions.
61///
62/// This enum provides variants for the most common token instructions,
63/// with a catch-all variant for other instruction types.
64#[derive(Debug)]
65pub enum TokenInstruction {
66    /// A simple transfer instruction
67    Transfer { amount: u64 },
68    /// A transfer with decimal checking
69    TransferChecked { amount: u64, decimals: u8 },
70    /// Catch-all variant for other instruction types
71    Other,
72}
73
74/// Implementation of the Solana token program functionality.
75///
76/// This struct provides concrete implementations for the SolanaToken trait,
77/// supporting both the SPL Token and Token-2022 programs.
78pub struct SolanaTokenProgram;
79
80impl SolanaTokenProgram {
81    /// Get the token program for a mint
82    pub async fn get_token_program_for_mint<P: SolanaProviderTrait>(
83        provider: &P,
84        mint: &Pubkey,
85    ) -> Result<Pubkey, TokenError> {
86        let account = provider
87            .get_account_from_pubkey(mint)
88            .await
89            .map_err(|e| TokenError::InvalidTokenMint(e.to_string()))?;
90
91        if account.owner == spl_token::id() {
92            Ok(spl_token::id())
93        } else if account.owner == spl_token_2022::id() {
94            Ok(spl_token_2022::id())
95        } else {
96            Err(TokenError::InvalidTokenProgram(format!(
97                "Unknown token program: {}",
98                account.owner
99            )))
100        }
101    }
102
103    /// Checks if a program ID corresponds to a known token program.
104    ///
105    /// # Arguments
106    ///
107    /// * `program_id` - The program ID to check
108    ///
109    /// # Returns
110    ///
111    /// `true` if the program ID is SPL Token or Token-2022, `false` otherwise
112    pub fn is_token_program(program_id: &Pubkey) -> bool {
113        program_id == &spl_token::id() || program_id == &spl_token_2022::id()
114    }
115
116    /// Creates a transfer checked instruction.
117    ///
118    /// # Arguments
119    ///
120    /// * `program_id` - The program ID of the token program
121    /// * `source` - The source token account
122    /// * `mint` - The mint address
123    /// * `destination` - The destination token account
124    /// * `authority` - The authority that can sign for the source account
125    /// * `amount` - The amount to transfer
126    /// * `decimals` - The number of decimals for the token
127    ///
128    /// # Returns
129    ///
130    /// A Result containing either the transfer instruction or a TokenError
131    pub fn create_transfer_checked_instruction(
132        program_id: &Pubkey,
133        source: &Pubkey,
134        mint: &Pubkey,
135        destination: &Pubkey,
136        authority: &Pubkey,
137        amount: u64,
138        decimals: u8,
139    ) -> Result<Instruction, TokenError> {
140        if !Self::is_token_program(program_id) {
141            return Err(TokenError::InvalidTokenProgram(format!(
142                "Unknown token program: {}",
143                program_id
144            )));
145        }
146        if program_id == &spl_token::id() {
147            return spl_token::instruction::transfer_checked(
148                program_id,
149                source,
150                mint,
151                destination,
152                authority,
153                &[],
154                amount,
155                decimals,
156            )
157            .map_err(|e| TokenError::Instruction(e.to_string()));
158        } else if program_id == &spl_token_2022::id() {
159            return spl_token_2022::instruction::transfer_checked(
160                program_id,
161                source,
162                mint,
163                destination,
164                authority,
165                &[],
166                amount,
167                decimals,
168            )
169            .map_err(|e| TokenError::Instruction(e.to_string()));
170        }
171        Err(TokenError::InvalidTokenProgram(format!(
172            "Unknown token program: {}",
173            program_id
174        )))
175    }
176
177    /// Unpacks a Solana account into a TokenAccount structure.
178    ///
179    /// # Arguments
180    ///
181    /// * `program_id` - The program ID of the token program
182    /// * `account` - The Solana account to unpack
183    ///
184    /// # Returns
185    ///
186    /// A Result containing either the unpacked TokenAccount or a TokenError
187    pub fn unpack_account(
188        program_id: &Pubkey,
189        account: &SolanaAccount,
190    ) -> Result<TokenAccount, TokenError> {
191        if !Self::is_token_program(program_id) {
192            return Err(TokenError::InvalidTokenProgram(format!(
193                "Unknown token program: {}",
194                program_id
195            )));
196        }
197        if program_id == &spl_token::id() {
198            let account = SplTokenAccount::unpack(&account.data)
199                .map_err(|e| TokenError::AccountError(format!("Invalid token account1: {}", e)))?;
200
201            return Ok(TokenAccount {
202                mint: account.mint,
203                owner: account.owner,
204                amount: account.amount,
205                is_frozen: account.is_frozen(),
206            });
207        } else if program_id == &spl_token_2022::id() {
208            let state_with_extensions = spl_token_2022::extension::StateWithExtensions::<
209                spl_token_2022::state::Account,
210            >::unpack(&account.data)
211            .map_err(|e| TokenError::AccountError(format!("Invalid token account2: {}", e)))?;
212
213            let base_account = state_with_extensions.base;
214
215            return Ok(TokenAccount {
216                mint: base_account.mint,
217                owner: base_account.owner,
218                amount: base_account.amount,
219                is_frozen: base_account.is_frozen(),
220            });
221        }
222        Err(TokenError::InvalidTokenProgram(format!(
223            "Unknown token program: {}",
224            program_id
225        )))
226    }
227
228    /// Gets the associated token address for a wallet and mint.
229    ///
230    /// # Arguments
231    ///
232    /// * `program_id` - The program ID of the token program
233    /// * `wallet` - The wallet address
234    /// * `mint` - The mint address
235    ///
236    /// # Returns
237    ///
238    /// The associated token address
239    pub fn get_associated_token_address(
240        program_id: &Pubkey,
241        wallet: &Pubkey,
242        mint: &Pubkey,
243    ) -> Pubkey {
244        get_associated_token_address_with_program_id(wallet, mint, program_id)
245    }
246
247    /// Creates an instruction to create an associated token account.
248    ///
249    /// # Arguments
250    ///
251    /// * `program_id` - The program ID of the token program
252    /// * `payer` - The account that will pay for the account creation
253    /// * `wallet` - The wallet address
254    /// * `mint` - The mint address
255    ///
256    /// # Returns
257    ///
258    /// An instruction to create the associated token account
259    pub fn create_associated_token_account(
260        program_id: &Pubkey,
261        payer: &Pubkey,
262        wallet: &Pubkey,
263        mint: &Pubkey,
264    ) -> Instruction {
265        create_associated_token_account(payer, wallet, mint, program_id)
266    }
267
268    /// Unpacks a token instruction from its binary data.
269    ///
270    /// # Arguments
271    ///
272    /// * `program_id` - The program ID of the token program
273    /// * `data` - The binary instruction data
274    ///
275    /// # Returns
276    ///
277    /// A Result containing either the unpacked TokenInstruction or a TokenError
278    pub fn unpack_instruction(
279        program_id: &Pubkey,
280        data: &[u8],
281    ) -> Result<TokenInstruction, TokenError> {
282        if !Self::is_token_program(program_id) {
283            return Err(TokenError::InvalidTokenProgram(format!(
284                "Unknown token program: {}",
285                program_id
286            )));
287        }
288        if program_id == &spl_token::id() {
289            match spl_token::instruction::TokenInstruction::unpack(data) {
290                Ok(instr) => match instr {
291                    spl_token::instruction::TokenInstruction::Transfer { amount } => {
292                        Ok(TokenInstruction::Transfer { amount })
293                    }
294                    spl_token::instruction::TokenInstruction::TransferChecked {
295                        amount,
296                        decimals,
297                    } => Ok(TokenInstruction::TransferChecked { amount, decimals }),
298                    _ => Ok(TokenInstruction::Other), // Catch all other instruction types
299                },
300                Err(e) => Err(TokenError::InvalidTokenInstruction(e.to_string())),
301            }
302        } else if program_id == &spl_token_2022::id() {
303            match spl_token_2022::instruction::TokenInstruction::unpack(data) {
304                Ok(instr) => match instr {
305                    #[allow(deprecated)]
306                    spl_token_2022::instruction::TokenInstruction::Transfer { amount } => {
307                        Ok(TokenInstruction::Transfer { amount })
308                    }
309                    spl_token_2022::instruction::TokenInstruction::TransferChecked {
310                        amount,
311                        decimals,
312                    } => Ok(TokenInstruction::TransferChecked { amount, decimals }),
313                    _ => Ok(TokenInstruction::Other), // Catch all other instruction types
314                },
315                Err(e) => Err(TokenError::InvalidTokenInstruction(e.to_string())),
316            }
317        } else {
318            Err(TokenError::InvalidTokenProgram(format!(
319                "Unknown token program: {}",
320                program_id
321            )))
322        }
323    }
324
325    /// Gets a token account for a given owner and mint, then unpacks it into a TokenAccount struct.
326    ///
327    /// This is a convenience method that combines several operations:
328    /// 1. Finds the appropriate token program for the mint
329    /// 2. Derives the associated token account address
330    /// 3. Fetches the account data
331    /// 4. Unpacks it into a structured TokenAccount
332    ///
333    /// # Arguments
334    ///
335    /// * `provider` - The Solana provider to use for RPC calls
336    /// * `owner` - The public key of the token owner
337    /// * `mint` - The public key of the token mint
338    ///
339    /// # Returns
340    ///
341    /// A Result containing the unpacked TokenAccount or a TokenError
342    pub async fn get_and_unpack_token_account<P: SolanaProviderTrait>(
343        provider: &P,
344        owner: &Pubkey,
345        mint: &Pubkey,
346    ) -> Result<TokenAccount, TokenError> {
347        // Get the token program ID for this mint
348        let program_id = Self::get_token_program_for_mint(provider, mint).await?;
349
350        // Derive the associated token account address
351        let token_account_address = Self::get_associated_token_address(&program_id, owner, mint);
352
353        // Fetch the token account data
354        let account_data = provider
355            .get_account_from_pubkey(&token_account_address)
356            .await
357            .map_err(|e| {
358                TokenError::AccountError(format!(
359                    "Failed to fetch token account for owner {} and mint {}: {}",
360                    owner, mint, e
361                ))
362            })?;
363
364        // Unpack the token account data
365        Self::unpack_account(&program_id, &account_data)
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use mockall::predicate::eq;
372    use solana_sdk::{program_pack::Pack, pubkey::Pubkey};
373    use spl_associated_token_account::get_associated_token_address_with_program_id;
374    use spl_associated_token_account::instruction::create_associated_token_account;
375    use spl_token::state::Account;
376
377    use crate::{
378        domain::{SolanaTokenProgram, TokenError, TokenInstruction},
379        services::MockSolanaProviderTrait,
380    };
381
382    #[tokio::test]
383    async fn test_get_token_program_for_mint_spl_token() {
384        let mint = Pubkey::new_unique();
385        let mut mock_provider = MockSolanaProviderTrait::new();
386
387        mock_provider
388            .expect_get_account_from_pubkey()
389            .with(eq(mint))
390            .times(1)
391            .returning(|_| {
392                Box::pin(async {
393                    Ok(solana_sdk::account::Account {
394                        lamports: 1000000,
395                        data: vec![],
396                        owner: spl_token::id(),
397                        executable: false,
398                        rent_epoch: 0,
399                    })
400                })
401            });
402
403        let result = SolanaTokenProgram::get_token_program_for_mint(&mock_provider, &mint).await;
404
405        assert!(result.is_ok());
406        assert_eq!(result.unwrap(), spl_token::id());
407    }
408
409    #[tokio::test]
410    async fn test_get_token_program_for_mint_token_2022() {
411        let mint = Pubkey::new_unique();
412        let mut mock_provider = MockSolanaProviderTrait::new();
413
414        mock_provider
415            .expect_get_account_from_pubkey()
416            .with(eq(mint))
417            .times(1)
418            .returning(|_| {
419                Box::pin(async {
420                    Ok(solana_sdk::account::Account {
421                        lamports: 1000000,
422                        data: vec![],
423                        owner: spl_token_2022::id(),
424                        executable: false,
425                        rent_epoch: 0,
426                    })
427                })
428            });
429
430        let result = SolanaTokenProgram::get_token_program_for_mint(&mock_provider, &mint).await;
431        assert!(result.is_ok());
432        assert_eq!(result.unwrap(), spl_token_2022::id());
433    }
434
435    #[tokio::test]
436    async fn test_get_token_program_for_mint_invalid() {
437        let mint = Pubkey::new_unique();
438        let mut mock_provider = MockSolanaProviderTrait::new();
439
440        mock_provider
441            .expect_get_account_from_pubkey()
442            .with(eq(mint))
443            .times(1)
444            .returning(|_| {
445                Box::pin(async {
446                    Ok(solana_sdk::account::Account {
447                        lamports: 1000000,
448                        data: vec![],
449                        owner: Pubkey::new_unique(),
450                        executable: false,
451                        rent_epoch: 0,
452                    })
453                })
454            });
455
456        let result = SolanaTokenProgram::get_token_program_for_mint(&mock_provider, &mint).await;
457        assert!(result.is_err());
458        assert!(matches!(
459            result.unwrap_err(),
460            TokenError::InvalidTokenProgram(_)
461        ));
462    }
463
464    #[test]
465    fn test_is_token_program() {
466        assert!(SolanaTokenProgram::is_token_program(&spl_token::id()));
467        assert!(SolanaTokenProgram::is_token_program(&spl_token_2022::id()));
468        assert!(!SolanaTokenProgram::is_token_program(&Pubkey::new_unique()));
469    }
470
471    #[test]
472    fn test_create_transfer_checked_instruction_spl_token() {
473        let program_id = spl_token::id();
474        let source = Pubkey::new_unique();
475        let mint = Pubkey::new_unique();
476        let destination = Pubkey::new_unique();
477        let authority = Pubkey::new_unique();
478        let amount = 1000;
479        let decimals = 9;
480
481        let result = SolanaTokenProgram::create_transfer_checked_instruction(
482            &program_id,
483            &source,
484            &mint,
485            &destination,
486            &authority,
487            amount,
488            decimals,
489        );
490
491        assert!(result.is_ok());
492        let instruction = result.unwrap();
493        assert_eq!(instruction.program_id, program_id);
494        assert_eq!(instruction.accounts.len(), 4);
495        assert_eq!(instruction.accounts[0].pubkey, source);
496        assert_eq!(instruction.accounts[1].pubkey, mint);
497        assert_eq!(instruction.accounts[2].pubkey, destination);
498        assert_eq!(instruction.accounts[3].pubkey, authority);
499    }
500
501    #[test]
502    fn test_create_transfer_checked_instruction_token_2022() {
503        let program_id = spl_token_2022::id();
504        let source = Pubkey::new_unique();
505        let mint = Pubkey::new_unique();
506        let destination = Pubkey::new_unique();
507        let authority = Pubkey::new_unique();
508        let amount = 1000;
509        let decimals = 9;
510
511        let result = SolanaTokenProgram::create_transfer_checked_instruction(
512            &program_id,
513            &source,
514            &mint,
515            &destination,
516            &authority,
517            amount,
518            decimals,
519        );
520
521        assert!(result.is_ok());
522        let instruction = result.unwrap();
523        assert_eq!(instruction.program_id, program_id);
524        assert_eq!(instruction.accounts.len(), 4);
525        assert_eq!(instruction.accounts[0].pubkey, source);
526        assert_eq!(instruction.accounts[1].pubkey, mint);
527        assert_eq!(instruction.accounts[2].pubkey, destination);
528        assert_eq!(instruction.accounts[3].pubkey, authority);
529    }
530
531    #[test]
532    fn test_create_transfer_checked_instruction_invalid_program() {
533        let program_id = Pubkey::new_unique(); // Invalid program ID
534        let source = Pubkey::new_unique();
535        let mint = Pubkey::new_unique();
536        let destination = Pubkey::new_unique();
537        let authority = Pubkey::new_unique();
538        let amount = 1000;
539        let decimals = 9;
540
541        let result = SolanaTokenProgram::create_transfer_checked_instruction(
542            &program_id,
543            &source,
544            &mint,
545            &destination,
546            &authority,
547            amount,
548            decimals,
549        );
550
551        assert!(result.is_err());
552        assert!(matches!(
553            result.unwrap_err(),
554            TokenError::InvalidTokenProgram(_)
555        ));
556    }
557
558    #[test]
559    fn test_unpack_account_spl_token() {
560        let program_id = spl_token::id();
561        let mint = Pubkey::new_unique();
562        let owner = Pubkey::new_unique();
563        let amount = 1000;
564
565        let spl_account = Account {
566            mint,
567            owner,
568            amount,
569            state: spl_token::state::AccountState::Initialized,
570            ..Default::default()
571        };
572
573        let mut account_data = vec![0; Account::LEN];
574        Account::pack(spl_account, &mut account_data).unwrap();
575
576        let solana_account = solana_sdk::account::Account {
577            lamports: 0,
578            data: account_data,
579            owner: program_id,
580            executable: false,
581            rent_epoch: 0,
582        };
583
584        let result = SolanaTokenProgram::unpack_account(&program_id, &solana_account);
585        assert!(result.is_ok());
586
587        let token_account = result.unwrap();
588        assert_eq!(token_account.mint, mint);
589        assert_eq!(token_account.owner, owner);
590        assert_eq!(token_account.amount, amount);
591        assert!(!token_account.is_frozen);
592    }
593
594    #[test]
595    fn test_unpack_account_token_2022() {
596        let program_id = spl_token_2022::id();
597        let mint = Pubkey::new_unique();
598        let owner = Pubkey::new_unique();
599        let amount = 1000;
600
601        let spl_account = Account {
602            mint,
603            owner,
604            amount,
605            state: spl_token::state::AccountState::Initialized,
606            ..Default::default()
607        };
608
609        let mut account_data = vec![0; Account::LEN];
610        Account::pack(spl_account, &mut account_data).unwrap();
611
612        let solana_account = solana_sdk::account::Account {
613            lamports: 0,
614            data: account_data,
615            owner: program_id,
616            executable: false,
617            rent_epoch: 0,
618        };
619
620        let result = SolanaTokenProgram::unpack_account(&program_id, &solana_account);
621        assert!(result.is_ok());
622
623        let token_account = result.unwrap();
624        assert_eq!(token_account.mint, mint);
625        assert_eq!(token_account.owner, owner);
626        assert_eq!(token_account.amount, amount);
627        assert!(!token_account.is_frozen);
628    }
629
630    #[test]
631    fn test_unpack_account_invalid_program() {
632        let program_id = Pubkey::new_unique(); // Invalid program ID
633        let mint = Pubkey::new_unique();
634        let owner = Pubkey::new_unique();
635        let amount = 1000;
636
637        let spl_account = Account {
638            mint,
639            owner,
640            amount,
641            state: spl_token::state::AccountState::Initialized,
642            ..Default::default()
643        };
644
645        let mut account_data = vec![0; Account::LEN];
646        Account::pack(spl_account, &mut account_data).unwrap();
647
648        let account = solana_sdk::account::Account {
649            lamports: 0,
650            data: account_data,
651            owner: program_id,
652            executable: false,
653            rent_epoch: 0,
654        };
655
656        let result = SolanaTokenProgram::unpack_account(&program_id, &account);
657        assert!(result.is_err());
658        assert!(matches!(
659            result.unwrap_err(),
660            TokenError::InvalidTokenProgram(_)
661        ));
662    }
663
664    #[test]
665    fn test_get_associated_token_address_spl_token() {
666        let program_id = spl_token::id();
667        let wallet = Pubkey::new_unique();
668        let mint = Pubkey::new_unique();
669
670        let result = SolanaTokenProgram::get_associated_token_address(&program_id, &wallet, &mint);
671        let expected = get_associated_token_address_with_program_id(&wallet, &mint, &program_id);
672
673        assert_eq!(result, expected);
674    }
675
676    #[test]
677    fn test_get_associated_token_address_token_2022() {
678        let program_id = spl_token_2022::id();
679        let wallet = Pubkey::new_unique();
680        let mint = Pubkey::new_unique();
681
682        let result = SolanaTokenProgram::get_associated_token_address(&program_id, &wallet, &mint);
683        let expected = get_associated_token_address_with_program_id(&wallet, &mint, &program_id);
684
685        assert_eq!(result, expected);
686    }
687
688    #[test]
689    fn test_create_associated_token_account() {
690        let program_id = spl_token::id();
691        let payer = Pubkey::new_unique();
692        let wallet = Pubkey::new_unique();
693        let mint = Pubkey::new_unique();
694
695        let instruction = SolanaTokenProgram::create_associated_token_account(
696            &program_id,
697            &payer,
698            &wallet,
699            &mint,
700        );
701
702        let expected = create_associated_token_account(&payer, &wallet, &mint, &program_id);
703
704        assert_eq!(instruction.program_id, expected.program_id);
705        assert_eq!(instruction.accounts.len(), expected.accounts.len());
706
707        for (i, account) in instruction.accounts.iter().enumerate() {
708            assert_eq!(account.pubkey, expected.accounts[i].pubkey);
709            assert_eq!(account.is_signer, expected.accounts[i].is_signer);
710            assert_eq!(account.is_writable, expected.accounts[i].is_writable);
711        }
712    }
713
714    #[test]
715    fn test_unpack_instruction_spl_token_transfer() {
716        let program_id = spl_token::id();
717        let amount = 1000u64;
718
719        let instruction = spl_token::instruction::transfer(
720            &program_id,
721            &Pubkey::new_unique(),
722            &Pubkey::new_unique(),
723            &Pubkey::new_unique(),
724            &[],
725            amount,
726        )
727        .unwrap();
728
729        let result = SolanaTokenProgram::unpack_instruction(&program_id, &instruction.data);
730        assert!(result.is_ok());
731
732        if let TokenInstruction::Transfer {
733            amount: parsed_amount,
734        } = result.unwrap()
735        {
736            assert_eq!(parsed_amount, amount);
737        } else {
738            panic!("Expected Transfer instruction");
739        }
740    }
741
742    #[test]
743    fn test_unpack_instruction_spl_token_transfer_checked() {
744        let program_id = spl_token::id();
745        let amount = 1000u64;
746        let decimals = 9u8;
747
748        let instruction = spl_token::instruction::transfer_checked(
749            &program_id,
750            &Pubkey::new_unique(),
751            &Pubkey::new_unique(),
752            &Pubkey::new_unique(),
753            &Pubkey::new_unique(),
754            &[],
755            amount,
756            decimals,
757        )
758        .unwrap();
759
760        let result = SolanaTokenProgram::unpack_instruction(&program_id, &instruction.data);
761        assert!(result.is_ok());
762
763        if let TokenInstruction::TransferChecked {
764            amount: parsed_amount,
765            decimals: parsed_decimals,
766        } = result.unwrap()
767        {
768            assert_eq!(parsed_amount, amount);
769            assert_eq!(parsed_decimals, decimals);
770        } else {
771            panic!("Expected TransferChecked instruction");
772        }
773    }
774
775    #[test]
776    fn test_unpack_instruction_token_2022_transfer() {
777        let program_id = spl_token_2022::id();
778        let amount = 1000u64;
779
780        #[allow(deprecated)]
781        let instruction = spl_token_2022::instruction::transfer(
782            &program_id,
783            &Pubkey::new_unique(),
784            &Pubkey::new_unique(),
785            &Pubkey::new_unique(),
786            &[],
787            amount,
788        )
789        .unwrap();
790
791        let result = SolanaTokenProgram::unpack_instruction(&program_id, &instruction.data);
792        assert!(result.is_ok());
793
794        if let TokenInstruction::Transfer {
795            amount: parsed_amount,
796        } = result.unwrap()
797        {
798            assert_eq!(parsed_amount, amount);
799        } else {
800            panic!("Expected Transfer instruction");
801        }
802    }
803
804    #[test]
805    fn test_unpack_instruction_token_2022_transfer_checked() {
806        let program_id = spl_token_2022::id();
807        let amount = 1000u64;
808        let decimals = 9u8;
809
810        let instruction = spl_token_2022::instruction::transfer_checked(
811            &program_id,
812            &Pubkey::new_unique(),
813            &Pubkey::new_unique(),
814            &Pubkey::new_unique(),
815            &Pubkey::new_unique(),
816            &[],
817            amount,
818            decimals,
819        )
820        .unwrap();
821
822        let result = SolanaTokenProgram::unpack_instruction(&program_id, &instruction.data);
823        assert!(result.is_ok());
824
825        if let TokenInstruction::TransferChecked {
826            amount: parsed_amount,
827            decimals: parsed_decimals,
828        } = result.unwrap()
829        {
830            assert_eq!(parsed_amount, amount);
831            assert_eq!(parsed_decimals, decimals);
832        } else {
833            panic!("Expected TransferChecked instruction");
834        }
835    }
836
837    #[test]
838    fn test_unpack_instruction_invalid_program() {
839        let program_id = Pubkey::new_unique(); // Invalid program ID
840        let data = vec![0, 1, 2, 3];
841
842        let result = SolanaTokenProgram::unpack_instruction(&program_id, &data);
843        assert!(result.is_err());
844        assert!(matches!(
845            result.unwrap_err(),
846            TokenError::InvalidTokenProgram(_)
847        ));
848    }
849}