testsvm_spl/
lib.rs

1//! # TestSVM SPL Helpers
2//!
3//! SPL Token helper functions for the TestSVM testing framework.
4//!
5//! This crate provides the `TestSVMSPLHelpers` trait that extends TestSVM
6//! with SPL Token-specific functionality for creating mints, token accounts,
7//! and performing token operations.
8
9use anchor_spl::token;
10use anyhow::{Context, Result, anyhow};
11use testsvm_core::prelude::*;
12
13pub mod prelude;
14
15/// SPL Token helper functions for TestSVM
16pub trait TestSVMSPLHelpers {
17    /// Create a mint with the test SVM's payer and add to address book
18    ///
19    /// # Arguments
20    ///
21    /// * `name` - Name for the mint in the address book
22    /// * `decimals` - Number of decimals for the token
23    /// * `authority` - Mint and freeze authority for the token
24    ///
25    /// # Example
26    ///
27    /// ```
28    /// use testsvm_core::prelude::*;
29    /// use testsvm_spl::TestSVMSPLHelpers;
30    ///
31    /// # fn main() -> anyhow::Result<()> {
32    /// let mut svm = TestSVM::init()?;
33    /// let authority = Keypair::new();
34    ///
35    /// // Create a mint with 6 decimals (like USDC)
36    /// let mint = svm.create_mint("usdc", 6, &authority.pubkey())?;
37    ///
38    /// // The mint is automatically added to the address book
39    /// assert!(svm.address_book.contains(&mint.key));
40    ///
41    /// // Read the mint information from chain
42    /// let mint_data: anchor_spl::token::Mint = mint.load(&svm)?;
43    /// assert_eq!(mint_data.decimals, 6);
44    /// assert_eq!(mint_data.mint_authority.unwrap(), authority.pubkey());
45    /// assert_eq!(mint_data.supply, 0);
46    /// assert!(mint_data.is_initialized);
47    /// # Ok(())
48    /// # }
49    /// ```
50    ///
51    /// # Example - Minting tokens
52    ///
53    /// ```
54    /// use testsvm_core::prelude::*;
55    /// use testsvm_spl::TestSVMSPLHelpers;
56    /// use anchor_spl::token;
57    ///
58    /// # fn main() -> anyhow::Result<()> {
59    /// let mut svm = TestSVM::init()?;
60    /// let authority = svm.new_wallet("authority")?;
61    /// let user = svm.new_wallet("user")?;
62    ///
63    /// // Create a mint
64    /// let mint = svm.create_mint("my_token", 9, &authority.pubkey())?;
65    ///
66    /// // Create an ATA for the user
67    /// let (create_ata_ix, user_ata) = svm.create_ata_ix(
68    ///     "user_ata",
69    ///     &user.pubkey(),
70    ///     &mint.key
71    /// )?;
72    /// svm.execute_ixs(&[create_ata_ix])?;
73    ///
74    /// // Mint 1000 tokens to the user (with 9 decimals)
75    /// let mint_amount = 1000 * 10u64.pow(9);
76    /// let mint_ix = token::spl_token::instruction::mint_to(
77    ///     &token::ID,
78    ///     &mint.key,
79    ///     &user_ata.key,
80    ///     &authority.pubkey(),
81    ///     &[],
82    ///     mint_amount,
83    /// )?;
84    /// svm.execute_ixs_with_signers(&[mint_ix], &[&authority])?;
85    ///
86    /// // Verify the mint supply was updated
87    /// let mint_data: token::Mint = mint.load(&svm)?;
88    /// assert_eq!(mint_data.supply, mint_amount);
89    ///
90    /// // Verify the user received the tokens
91    /// let ata_data: token::TokenAccount = user_ata.load(&svm)?;
92    /// assert_eq!(ata_data.amount, mint_amount);
93    /// # Ok(())
94    /// # }
95    /// ```
96    fn create_mint(
97        &mut self,
98        name: &str,
99        decimals: u8,
100        authority: &Pubkey,
101    ) -> Result<AccountRef<anchor_spl::token::Mint>>;
102
103    /// Create an associated token account instruction and add to address book
104    ///
105    /// Returns the instruction and the ATA address. The instruction must be executed
106    /// separately to actually create the account on-chain.
107    ///
108    /// # Arguments
109    ///
110    /// * `label` - Label for the ATA in the address book
111    /// * `owner` - Owner of the associated token account
112    /// * `mint` - Mint for which to create the ATA
113    ///
114    /// # Example
115    ///
116    /// ```
117    /// use testsvm_core::prelude::*;
118    /// use testsvm_spl::TestSVMSPLHelpers;
119    ///
120    /// # fn main() -> anyhow::Result<()> {
121    /// let mut svm = TestSVM::init()?;
122    /// let authority = Keypair::new();
123    /// let user = Keypair::new();
124    ///
125    /// // Create a mint first
126    /// let mint = svm.create_mint("token", 9, &authority.pubkey())?;
127    ///
128    /// // Create an ATA instruction for the user
129    /// let (ix, ata) = svm.create_ata_ix("user_token_ata", &user.pubkey(), &mint.key)?;
130    ///
131    /// // Execute the instruction to create the ATA on-chain
132    /// svm.execute_ixs(&[ix])?;
133    ///
134    /// // The ATA is automatically added to the address book
135    /// assert!(svm.address_book.contains(&ata.key));
136    ///
137    /// // Read the ATA information from chain
138    /// let ata_data: anchor_spl::token::TokenAccount = ata.load(&svm)?;
139    /// assert_eq!(ata_data.owner, user.pubkey());
140    /// assert_eq!(ata_data.mint, mint.key);
141    /// assert_eq!(ata_data.amount, 0);
142    /// assert_eq!(ata_data.state, anchor_spl::token::spl_token::state::AccountState::Initialized);
143    /// # Ok(())
144    /// # }
145    /// ```
146    fn create_ata_ix(
147        &mut self,
148        label: &str,
149        owner: &Pubkey,
150        mint: &Pubkey,
151    ) -> Result<(
152        solana_sdk::instruction::Instruction,
153        AccountRef<anchor_spl::token::TokenAccount>,
154    )>;
155}
156
157impl TestSVMSPLHelpers for TestSVM {
158    fn create_mint(
159        &mut self,
160        name: &str,
161        decimals: u8,
162        authority: &Pubkey,
163    ) -> Result<AccountRef<anchor_spl::token::Mint>> {
164        let mint = Keypair::new();
165
166        let rent = self
167            .svm
168            .minimum_balance_for_rent_exemption(token::Mint::LEN);
169
170        let create_account_ix = solana_sdk::system_instruction::create_account(
171            &self.default_fee_payer.pubkey(),
172            &mint.pubkey(),
173            rent,
174            anchor_spl::token::Mint::LEN as u64,
175            &anchor_spl::token::ID,
176        );
177
178        let init_mint_ix = anchor_spl::token::spl_token::instruction::initialize_mint(
179            &anchor_spl::token::ID,
180            &mint.pubkey(),
181            authority,
182            Some(authority), // Set freeze authority to same as mint authority
183            decimals,
184        )
185        .context("Failed to create initialize mint instruction")?;
186
187        // Add the mint to the address book
188        let mint_pubkey = mint.pubkey();
189        let label = format!("mint:{name}");
190        self.address_book
191            .add(mint_pubkey, label, RegisteredAddress::mint(mint_pubkey))?;
192
193        self.execute_ixs_with_signers(&[create_account_ix, init_mint_ix], &[&mint])
194            .map_err(|e| anyhow!("Failed to create mint: {}", e))?;
195
196        Ok(AccountRef::new(mint_pubkey))
197    }
198
199    fn create_ata_ix(
200        &mut self,
201        label: &str,
202        owner: &Pubkey,
203        mint: &Pubkey,
204    ) -> Result<(
205        solana_sdk::instruction::Instruction,
206        AccountRef<anchor_spl::token::TokenAccount>,
207    )> {
208        let ata = anchor_spl::associated_token::get_associated_token_address(owner, mint);
209
210        // Add to address book
211        self.address_book.add(
212            ata,
213            label.to_string(),
214            RegisteredAddress::ata(ata, *mint, *owner),
215        )?;
216
217        let ix = anchor_spl::associated_token::spl_associated_token_account::instruction::create_associated_token_account_idempotent(
218            &self.default_fee_payer(),
219            owner,
220            mint,
221            &anchor_spl::token::ID,
222        );
223
224        Ok((ix, AccountRef::new(ata)))
225    }
226}