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