light_token/instruction/
create.rs

1use borsh::BorshSerialize;
2use light_token_interface::instructions::{
3    create_token_account::CreateTokenAccountInstructionData,
4    extensions::{CompressToPubkey, 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
14/// # Create a create ctoken account instruction:
15/// ```rust
16/// # use solana_pubkey::Pubkey;
17/// # use light_token::instruction::CreateTokenAccount;
18/// # let payer = Pubkey::new_unique();
19/// # let account = Pubkey::new_unique();
20/// # let mint = Pubkey::new_unique();
21/// # let owner = Pubkey::new_unique();
22/// let instruction =
23///     CreateTokenAccount::new(payer, account, mint, owner)
24///     .instruction()?;
25/// # Ok::<(), solana_program_error::ProgramError>(())
26/// ```
27#[derive(Debug, Clone)]
28pub struct CreateTokenAccount {
29    pub payer: Pubkey,
30    pub account: Pubkey,
31    pub mint: Pubkey,
32    pub owner: Pubkey,
33    pub compressible: CompressibleParams,
34}
35
36impl CreateTokenAccount {
37    pub fn new(payer: Pubkey, account: Pubkey, mint: Pubkey, owner: Pubkey) -> Self {
38        Self {
39            payer,
40            account,
41            mint,
42            owner,
43            compressible: CompressibleParams::default(),
44        }
45    }
46
47    pub fn with_compressible(mut self, compressible: CompressibleParams) -> Self {
48        self.compressible = compressible;
49        self
50    }
51
52    pub fn instruction(self) -> Result<Instruction, ProgramError> {
53        let instruction_data = CreateTokenAccountInstructionData {
54            owner: light_compressed_account::Pubkey::from(self.owner.to_bytes()),
55            compressible_config: Some(CompressibleExtensionInstructionData {
56                token_account_version: self.compressible.token_account_version as u8,
57                rent_payment: self.compressible.pre_pay_num_epochs,
58                compression_only: self.compressible.compression_only as u8,
59                write_top_up: self.compressible.lamports_per_write.unwrap_or(0),
60                compress_to_account_pubkey: self.compressible.compress_to_account_pubkey,
61            }),
62        };
63
64        let mut data = Vec::new();
65        data.push(18u8); // InitializeAccount3 opcode
66        instruction_data
67            .serialize(&mut data)
68            .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
69
70        let accounts = vec![
71            AccountMeta::new(self.account, true),
72            AccountMeta::new_readonly(self.mint, false),
73            AccountMeta::new(self.payer, true),
74            AccountMeta::new_readonly(self.compressible.compressible_config, false),
75            AccountMeta::new_readonly(Pubkey::default(), false), // system_program
76            AccountMeta::new(self.compressible.rent_sponsor, false),
77        ];
78
79        Ok(Instruction {
80            program_id: Pubkey::from(light_token_interface::LIGHT_TOKEN_PROGRAM_ID),
81            accounts,
82            data,
83        })
84    }
85}
86
87/// CPI builder for creating CToken accounts (vaults).
88///
89/// # Example - Rent-free vault with PDA signing
90/// ```rust,ignore
91/// CreateTokenAccountCpi {
92///     payer: ctx.accounts.payer.to_account_info(),
93///     account: ctx.accounts.vault.to_account_info(),
94///     mint: ctx.accounts.mint.to_account_info(),
95///     owner: ctx.accounts.vault_authority.key(),
96/// }
97/// .rent_free(
98///     ctx.accounts.ctoken_config.to_account_info(),
99///     ctx.accounts.rent_sponsor.to_account_info(),
100///     ctx.accounts.system_program.to_account_info(),
101///     &crate::ID,
102/// )
103/// .invoke_signed(&[b"vault", mint.key().as_ref(), &[bump]])?;
104/// ```
105pub struct CreateTokenAccountCpi<'info> {
106    pub payer: AccountInfo<'info>,
107    pub account: AccountInfo<'info>,
108    pub mint: AccountInfo<'info>,
109    pub owner: Pubkey,
110}
111
112impl<'info> CreateTokenAccountCpi<'info> {
113    /// Enable rent-free mode with compressible config.
114    ///
115    /// Returns a builder that can call `.invoke()` or `.invoke_signed(seeds)`.
116    /// When using `invoke_signed`, the seeds are used for both PDA signing
117    /// and deriving the compress_to address.
118    pub fn rent_free(
119        self,
120        config: AccountInfo<'info>,
121        sponsor: AccountInfo<'info>,
122        system_program: AccountInfo<'info>,
123        program_id: &Pubkey,
124    ) -> CreateTokenAccountRentFreeCpi<'info> {
125        CreateTokenAccountRentFreeCpi {
126            base: self,
127            config,
128            sponsor,
129            system_program,
130            program_id: *program_id,
131        }
132    }
133
134    /// Invoke without rent-free (requires manually constructed compressible params).
135    pub fn invoke_with(
136        self,
137        compressible: CompressibleParamsCpi<'info>,
138    ) -> Result<(), ProgramError> {
139        LegacyCreateTokenAccountCpi {
140            payer: self.payer,
141            account: self.account,
142            mint: self.mint,
143            owner: self.owner,
144            compressible,
145        }
146        .invoke()
147    }
148
149    /// Invoke with signing, without rent-free (requires manually constructed compressible params).
150    pub fn invoke_signed_with(
151        self,
152        compressible: CompressibleParamsCpi<'info>,
153        signer_seeds: &[&[&[u8]]],
154    ) -> Result<(), ProgramError> {
155        LegacyCreateTokenAccountCpi {
156            payer: self.payer,
157            account: self.account,
158            mint: self.mint,
159            owner: self.owner,
160            compressible,
161        }
162        .invoke_signed(signer_seeds)
163    }
164}
165
166/// Rent-free enabled CToken account creation CPI.
167pub struct CreateTokenAccountRentFreeCpi<'info> {
168    base: CreateTokenAccountCpi<'info>,
169    config: AccountInfo<'info>,
170    sponsor: AccountInfo<'info>,
171    system_program: AccountInfo<'info>,
172    program_id: Pubkey,
173}
174
175impl<'info> CreateTokenAccountRentFreeCpi<'info> {
176    /// Invoke CPI for non-program-owned accounts.
177    pub fn invoke(self) -> Result<(), ProgramError> {
178        let defaults = CompressibleParams::default();
179
180        let cpi = LegacyCreateTokenAccountCpi {
181            payer: self.base.payer,
182            account: self.base.account,
183            mint: self.base.mint,
184            owner: self.base.owner,
185            compressible: CompressibleParamsCpi {
186                compressible_config: self.config,
187                rent_sponsor: self.sponsor,
188                system_program: self.system_program,
189                pre_pay_num_epochs: defaults.pre_pay_num_epochs,
190                lamports_per_write: defaults.lamports_per_write,
191                compress_to_account_pubkey: None,
192                token_account_version: defaults.token_account_version,
193                compression_only: defaults.compression_only,
194            },
195        };
196        cpi.invoke()
197    }
198
199    /// Invoke CPI with PDA signing for program-owned accounts.
200    ///
201    /// Seeds are used for both signing AND deriving the compress_to address.
202    pub fn invoke_signed(self, seeds: &[&[u8]]) -> Result<(), ProgramError> {
203        let defaults = CompressibleParams::default();
204
205        // Build CompressToPubkey from signer seeds
206        let bump = seeds.last().and_then(|s| s.first()).copied().unwrap_or(0);
207
208        let seed_vecs: Vec<Vec<u8>> = seeds
209            .iter()
210            .take(seeds.len().saturating_sub(1))
211            .map(|s| s.to_vec())
212            .collect();
213
214        let compress_to = CompressToPubkey {
215            bump,
216            program_id: self.program_id.to_bytes(),
217            seeds: seed_vecs,
218        };
219
220        let cpi = LegacyCreateTokenAccountCpi {
221            payer: self.base.payer,
222            account: self.base.account,
223            mint: self.base.mint,
224            owner: self.base.owner,
225            compressible: CompressibleParamsCpi {
226                compressible_config: self.config,
227                rent_sponsor: self.sponsor,
228                system_program: self.system_program,
229                pre_pay_num_epochs: defaults.pre_pay_num_epochs,
230                lamports_per_write: defaults.lamports_per_write,
231                compress_to_account_pubkey: Some(compress_to),
232                token_account_version: defaults.token_account_version,
233                compression_only: defaults.compression_only,
234            },
235        };
236        cpi.invoke_signed(&[seeds])
237    }
238}
239
240/// Internal legacy CPI struct with full compressible params.
241struct LegacyCreateTokenAccountCpi<'info> {
242    payer: AccountInfo<'info>,
243    account: AccountInfo<'info>,
244    mint: AccountInfo<'info>,
245    owner: Pubkey,
246    compressible: CompressibleParamsCpi<'info>,
247}
248
249impl<'info> LegacyCreateTokenAccountCpi<'info> {
250    fn instruction(&self) -> Result<Instruction, ProgramError> {
251        CreateTokenAccount {
252            payer: *self.payer.key,
253            account: *self.account.key,
254            mint: *self.mint.key,
255            owner: self.owner,
256            compressible: CompressibleParams {
257                compressible_config: *self.compressible.compressible_config.key,
258                rent_sponsor: *self.compressible.rent_sponsor.key,
259                pre_pay_num_epochs: self.compressible.pre_pay_num_epochs,
260                lamports_per_write: self.compressible.lamports_per_write,
261                compress_to_account_pubkey: self.compressible.compress_to_account_pubkey.clone(),
262                token_account_version: self.compressible.token_account_version,
263                compression_only: self.compressible.compression_only,
264            },
265        }
266        .instruction()
267    }
268
269    fn invoke(self) -> Result<(), ProgramError> {
270        let instruction = self.instruction()?;
271        let account_infos = [
272            self.account,
273            self.mint,
274            self.payer,
275            self.compressible.compressible_config,
276            self.compressible.system_program,
277            self.compressible.rent_sponsor,
278        ];
279        invoke(&instruction, &account_infos)
280    }
281
282    fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
283        let instruction = self.instruction()?;
284        let account_infos = [
285            self.account,
286            self.mint,
287            self.payer,
288            self.compressible.compressible_config,
289            self.compressible.system_program,
290            self.compressible.rent_sponsor,
291        ];
292        invoke_signed(&instruction, &account_infos, signer_seeds)
293    }
294}