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}