Skip to main content

light_token/instruction/
create_ata.rs

1use borsh::BorshSerialize;
2use light_token_interface::instructions::{
3    create_associated_token_account::CreateAssociatedTokenAccountInstructionData,
4    extensions::CompressibleExtensionInstructionData,
5};
6use solana_account_info::AccountInfo;
7use solana_cpi::{invoke, invoke_signed};
8use solana_instruction::{AccountMeta, Instruction};
9use solana_program_error::ProgramError;
10use solana_pubkey::Pubkey;
11
12use crate::instruction::{compressible::CompressibleParamsCpi, CompressibleParams};
13
14const CREATE_ATA_DISCRIMINATOR: u8 = 100;
15const CREATE_ATA_IDEMPOTENT_DISCRIMINATOR: u8 = 102;
16
17pub fn derive_associated_token_account(owner: &Pubkey, mint: &Pubkey) -> Pubkey {
18    Pubkey::find_program_address(
19        &[
20            owner.as_ref(),
21            light_token_interface::LIGHT_TOKEN_PROGRAM_ID.as_ref(),
22            mint.as_ref(),
23        ],
24        &Pubkey::from(light_token_interface::LIGHT_TOKEN_PROGRAM_ID),
25    )
26    .0
27}
28
29/// # Create an associated ctoken account instruction:
30/// ```rust
31/// # use solana_pubkey::Pubkey;
32/// # use light_token::instruction::CreateAssociatedTokenAccount;
33/// # let payer = Pubkey::new_unique();
34/// # let owner = Pubkey::new_unique();
35/// # let mint = Pubkey::new_unique();
36/// let instruction =
37///     CreateAssociatedTokenAccount::new(payer, owner, mint)
38///     .instruction()?;
39/// # Ok::<(), solana_program_error::ProgramError>(())
40/// ```
41#[derive(Debug, Clone)]
42pub struct CreateAssociatedTokenAccount {
43    pub payer: Pubkey,
44    pub owner: Pubkey,
45    pub mint: Pubkey,
46    pub associated_token_account: Pubkey,
47    pub compressible: CompressibleParams,
48    pub idempotent: bool,
49}
50
51impl CreateAssociatedTokenAccount {
52    pub fn new(payer: Pubkey, owner: Pubkey, mint: Pubkey) -> Self {
53        let ata = derive_associated_token_account(&owner, &mint);
54        Self {
55            payer,
56            owner,
57            mint,
58            associated_token_account: ata,
59            compressible: CompressibleParams::default_ata(),
60            idempotent: false,
61        }
62    }
63
64    pub fn with_compressible(mut self, compressible_params: CompressibleParams) -> Self {
65        self.compressible = compressible_params;
66        self
67    }
68
69    pub fn idempotent(mut self) -> Self {
70        self.idempotent = true;
71        self
72    }
73
74    pub fn instruction(self) -> Result<Instruction, ProgramError> {
75        let instruction_data = CreateAssociatedTokenAccountInstructionData {
76            compressible_config: Some(CompressibleExtensionInstructionData {
77                token_account_version: self.compressible.token_account_version as u8,
78                rent_payment: self.compressible.pre_pay_num_epochs,
79                compression_only: self.compressible.compression_only as u8,
80                write_top_up: self.compressible.lamports_per_write.unwrap_or(0),
81                compress_to_account_pubkey: self.compressible.compress_to_account_pubkey.clone(),
82            }),
83        };
84
85        let discriminator = if self.idempotent {
86            CREATE_ATA_IDEMPOTENT_DISCRIMINATOR
87        } else {
88            CREATE_ATA_DISCRIMINATOR
89        };
90
91        let mut data = Vec::new();
92        data.push(discriminator);
93        instruction_data
94            .serialize(&mut data)
95            .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
96
97        let accounts = vec![
98            AccountMeta::new_readonly(self.owner, false),
99            AccountMeta::new_readonly(self.mint, false),
100            AccountMeta::new(self.payer, true),
101            AccountMeta::new(self.associated_token_account, false),
102            AccountMeta::new_readonly(Pubkey::new_from_array([0; 32]), false), // system_program
103            AccountMeta::new_readonly(self.compressible.compressible_config, false),
104            AccountMeta::new(self.compressible.rent_sponsor, false),
105        ];
106
107        Ok(Instruction {
108            program_id: Pubkey::from(light_token_interface::LIGHT_TOKEN_PROGRAM_ID),
109            accounts,
110            data,
111        })
112    }
113}
114
115/// CPI builder for creating CToken ATAs.
116///
117/// # Example - Rent-free ATA (idempotent)
118/// ```rust,ignore
119/// CreateTokenAtaCpi {
120///     payer: ctx.accounts.payer.to_account_info(),
121///     owner: ctx.accounts.owner.to_account_info(),
122///     mint: ctx.accounts.mint.to_account_info(),
123///     ata: ctx.accounts.user_ata.to_account_info(),
124/// }
125/// .idempotent()
126/// .rent_free(
127///     ctx.accounts.ctoken_config.to_account_info(),
128///     ctx.accounts.rent_sponsor.to_account_info(),
129///     ctx.accounts.system_program.to_account_info(),
130/// )
131/// .invoke()?;
132/// ```
133pub struct CreateTokenAtaCpi<'info> {
134    pub payer: AccountInfo<'info>,
135    pub owner: AccountInfo<'info>,
136    pub mint: AccountInfo<'info>,
137    pub ata: AccountInfo<'info>,
138}
139
140impl<'info> CreateTokenAtaCpi<'info> {
141    /// Make this an idempotent create (won't fail if ATA already exists).
142    pub fn idempotent(self) -> CreateTokenAtaCpiIdempotent<'info> {
143        CreateTokenAtaCpiIdempotent { base: self }
144    }
145
146    /// Enable rent-free mode with compressible config.
147    pub fn rent_free(
148        self,
149        config: AccountInfo<'info>,
150        sponsor: AccountInfo<'info>,
151        system_program: AccountInfo<'info>,
152    ) -> CreateTokenAtaRentFreeCpi<'info> {
153        CreateTokenAtaRentFreeCpi {
154            payer: self.payer,
155            owner: self.owner,
156            mint: self.mint,
157            ata: self.ata,
158            idempotent: false,
159            config,
160            sponsor,
161            system_program,
162        }
163    }
164
165    /// Invoke without rent-free (requires manually constructed compressible params).
166    pub fn invoke_with(
167        self,
168        compressible: CompressibleParamsCpi<'info>,
169        system_program: AccountInfo<'info>,
170    ) -> Result<(), ProgramError> {
171        InternalCreateAtaCpi {
172            owner: self.owner,
173            mint: self.mint,
174            payer: self.payer,
175            associated_token_account: self.ata,
176            system_program,
177            compressible,
178            idempotent: false,
179        }
180        .invoke()
181    }
182}
183
184/// Idempotent ATA creation (intermediate type).
185pub struct CreateTokenAtaCpiIdempotent<'info> {
186    base: CreateTokenAtaCpi<'info>,
187}
188
189impl<'info> CreateTokenAtaCpiIdempotent<'info> {
190    /// Enable rent-free mode with compressible config.
191    pub fn rent_free(
192        self,
193        config: AccountInfo<'info>,
194        sponsor: AccountInfo<'info>,
195        system_program: AccountInfo<'info>,
196    ) -> CreateTokenAtaRentFreeCpi<'info> {
197        CreateTokenAtaRentFreeCpi {
198            payer: self.base.payer,
199            owner: self.base.owner,
200            mint: self.base.mint,
201            ata: self.base.ata,
202            idempotent: true,
203            config,
204            sponsor,
205            system_program,
206        }
207    }
208
209    /// Invoke without rent-free (requires manually constructed compressible params).
210    pub fn invoke_with(
211        self,
212        compressible: CompressibleParamsCpi<'info>,
213        system_program: AccountInfo<'info>,
214    ) -> Result<(), ProgramError> {
215        InternalCreateAtaCpi {
216            owner: self.base.owner,
217            mint: self.base.mint,
218            payer: self.base.payer,
219            associated_token_account: self.base.ata,
220            system_program,
221            compressible,
222            idempotent: true,
223        }
224        .invoke()
225    }
226}
227
228/// Rent-free enabled CToken ATA creation CPI.
229pub struct CreateTokenAtaRentFreeCpi<'info> {
230    payer: AccountInfo<'info>,
231    owner: AccountInfo<'info>,
232    mint: AccountInfo<'info>,
233    ata: AccountInfo<'info>,
234    idempotent: bool,
235    config: AccountInfo<'info>,
236    sponsor: AccountInfo<'info>,
237    system_program: AccountInfo<'info>,
238}
239
240impl<'info> CreateTokenAtaRentFreeCpi<'info> {
241    /// Invoke CPI.
242    pub fn invoke(self) -> Result<(), ProgramError> {
243        InternalCreateAtaCpi {
244            owner: self.owner,
245            mint: self.mint,
246            payer: self.payer,
247            associated_token_account: self.ata,
248            system_program: self.system_program.clone(),
249            compressible: CompressibleParamsCpi::new_ata(
250                self.config,
251                self.sponsor,
252                self.system_program,
253            ),
254            idempotent: self.idempotent,
255        }
256        .invoke()
257    }
258
259    /// Invoke CPI with signer seeds (when caller needs to sign for another account).
260    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
261        InternalCreateAtaCpi {
262            owner: self.owner,
263            mint: self.mint,
264            payer: self.payer,
265            associated_token_account: self.ata,
266            system_program: self.system_program.clone(),
267            compressible: CompressibleParamsCpi::new_ata(
268                self.config,
269                self.sponsor,
270                self.system_program,
271            ),
272            idempotent: self.idempotent,
273        }
274        .invoke_signed(signer_seeds)
275    }
276}
277
278/// Internal CPI struct for ATAs with full params.
279struct InternalCreateAtaCpi<'info> {
280    owner: AccountInfo<'info>,
281    mint: AccountInfo<'info>,
282    payer: AccountInfo<'info>,
283    associated_token_account: AccountInfo<'info>,
284    system_program: AccountInfo<'info>,
285    compressible: CompressibleParamsCpi<'info>,
286    idempotent: bool,
287}
288
289impl<'info> InternalCreateAtaCpi<'info> {
290    fn instruction(&self) -> Result<Instruction, ProgramError> {
291        CreateAssociatedTokenAccount {
292            payer: *self.payer.key,
293            owner: *self.owner.key,
294            mint: *self.mint.key,
295            associated_token_account: *self.associated_token_account.key,
296            compressible: CompressibleParams {
297                compressible_config: *self.compressible.compressible_config.key,
298                rent_sponsor: *self.compressible.rent_sponsor.key,
299                pre_pay_num_epochs: self.compressible.pre_pay_num_epochs,
300                lamports_per_write: self.compressible.lamports_per_write,
301                compress_to_account_pubkey: self.compressible.compress_to_account_pubkey.clone(),
302                token_account_version: self.compressible.token_account_version,
303                compression_only: self.compressible.compression_only,
304            },
305            idempotent: self.idempotent,
306        }
307        .instruction()
308    }
309
310    fn invoke(self) -> Result<(), ProgramError> {
311        let instruction = self.instruction()?;
312        let account_infos = [
313            self.owner,
314            self.mint,
315            self.payer,
316            self.associated_token_account,
317            self.system_program,
318            self.compressible.compressible_config,
319            self.compressible.rent_sponsor,
320        ];
321        invoke(&instruction, &account_infos)
322    }
323
324    fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
325        let instruction = self.instruction()?;
326        let account_infos = [
327            self.owner,
328            self.mint,
329            self.payer,
330            self.associated_token_account,
331            self.system_program,
332            self.compressible.compressible_config,
333            self.compressible.rent_sponsor,
334        ];
335        invoke_signed(&instruction, &account_infos, signer_seeds)
336    }
337}