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