1#[cfg(feature = "anchor")]
4use anchor_lang::{AnchorDeserialize, AnchorSerialize};
5#[cfg(not(feature = "anchor"))]
6use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize};
7use light_account::{
8 CompressedAccountData, InitializeLightConfigParams, Pack, UpdateLightConfigParams,
9};
10use light_sdk::instruction::{
11 account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts,
12 SystemAccountMetaConfig, ValidityProof,
13};
14use light_token::constants::{
15 LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_CPI_AUTHORITY, LIGHT_TOKEN_PROGRAM_ID,
16 RENT_SPONSOR_V1 as RENT_SPONSOR,
17};
18use solana_instruction::{AccountMeta, Instruction};
19use solana_pubkey::Pubkey;
20
21use crate::indexer::{CompressedAccount, TreeInfo, ValidityProofWithContext};
22
23#[inline]
24fn get_output_queue(tree_info: &TreeInfo) -> Pubkey {
25 tree_info
26 .next_tree_info
27 .as_ref()
28 .map(|next| next.queue)
29 .unwrap_or(tree_info.queue)
30}
31
32#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
33pub struct LoadAccountsData<T> {
34 pub system_accounts_offset: u8,
35 pub token_accounts_offset: u8,
36 pub output_queue_index: u8,
37 pub proof: ValidityProof,
38 pub compressed_accounts: Vec<CompressedAccountData<T>>,
39}
40
41#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
42pub struct SaveAccountsData {
43 pub proof: ValidityProof,
44 pub compressed_accounts: Vec<CompressedAccountMetaNoLamportsNoAddress>,
45 pub system_accounts_offset: u8,
46}
47
48pub const INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] =
50 [133, 228, 12, 169, 56, 76, 222, 61];
51pub const UPDATE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = [135, 215, 243, 81, 163, 146, 33, 70];
52pub const DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] =
53 [114, 67, 61, 123, 234, 31, 1, 112];
54pub const COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] =
55 [70, 236, 171, 120, 164, 93, 113, 181];
56
57pub mod load {
59 use super::*;
60
61 pub fn accounts(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey) -> Vec<AccountMeta> {
63 vec![
64 AccountMeta::new(fee_payer, true),
65 AccountMeta::new_readonly(config, false),
66 AccountMeta::new(rent_sponsor, false),
67 AccountMeta::new(RENT_SPONSOR, false),
68 AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false),
69 AccountMeta::new_readonly(LIGHT_TOKEN_CPI_AUTHORITY, false),
70 AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false),
71 ]
72 }
73
74 pub fn accounts_pda_only(
76 fee_payer: Pubkey,
77 config: Pubkey,
78 rent_sponsor: Pubkey,
79 ) -> Vec<AccountMeta> {
80 vec![
81 AccountMeta::new(fee_payer, true),
82 AccountMeta::new_readonly(config, false),
83 AccountMeta::new(rent_sponsor, false),
84 AccountMeta::new(rent_sponsor, false), AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false),
86 AccountMeta::new_readonly(LIGHT_TOKEN_CPI_AUTHORITY, false),
87 AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false),
88 ]
89 }
90}
91
92#[allow(clippy::too_many_arguments)]
93pub fn initialize_config(
94 program_id: &Pubkey,
95 discriminator: &[u8],
96 payer: &Pubkey,
97 authority: &Pubkey,
98 rent_sponsor: Pubkey,
99 address_space: Vec<Pubkey>,
100 config_bump: Option<u8>,
101) -> Instruction {
102 let config_bump = config_bump.unwrap_or(0);
103 let config_bump_u16 = config_bump as u16;
104 let (config_pda, _) = Pubkey::find_program_address(
105 &[
106 light_account::LIGHT_CONFIG_SEED,
107 &config_bump_u16.to_le_bytes(),
108 ],
109 program_id,
110 );
111
112 let bpf_loader = solana_pubkey::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111");
113 let (program_data_pda, _) = Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader);
114
115 let system_program = solana_pubkey::pubkey!("11111111111111111111111111111111");
116 let accounts = vec![
117 AccountMeta::new(*payer, true),
118 AccountMeta::new(config_pda, false),
119 AccountMeta::new_readonly(program_data_pda, false),
120 AccountMeta::new_readonly(*authority, true),
121 AccountMeta::new_readonly(system_program, false),
122 ];
123
124 let params = InitializeLightConfigParams {
125 rent_sponsor: rent_sponsor.to_bytes(),
126 compression_authority: authority.to_bytes(),
127 rent_config: Default::default(),
128 write_top_up: 0,
129 address_space: address_space.iter().map(|p| p.to_bytes()).collect(),
130 config_bump,
131 };
132 let serialized = params.try_to_vec().expect("serialize params");
133 let mut data = Vec::with_capacity(discriminator.len() + serialized.len());
134 data.extend_from_slice(discriminator);
135 data.extend_from_slice(&serialized);
136
137 Instruction {
138 program_id: *program_id,
139 accounts,
140 data,
141 }
142}
143
144pub fn update_config(
145 program_id: &Pubkey,
146 discriminator: &[u8],
147 authority: &Pubkey,
148 new_rent_sponsor: Option<Pubkey>,
149 new_address_space: Option<Vec<Pubkey>>,
150 new_update_authority: Option<Pubkey>,
151) -> Instruction {
152 let (config_pda, _) = Pubkey::find_program_address(
153 &[light_account::LIGHT_CONFIG_SEED, &0u16.to_le_bytes()],
154 program_id,
155 );
156
157 let accounts = vec![
158 AccountMeta::new(config_pda, false),
159 AccountMeta::new_readonly(*authority, true),
160 ];
161
162 let params = UpdateLightConfigParams {
163 new_update_authority: new_update_authority.map(|p| p.to_bytes()),
164 new_rent_sponsor: new_rent_sponsor.map(|p| p.to_bytes()),
165 new_compression_authority: None,
166 new_rent_config: None,
167 new_write_top_up: None,
168 new_address_space: new_address_space.map(|v| v.iter().map(|p| p.to_bytes()).collect()),
169 };
170 let serialized = params.try_to_vec().expect("serialize params");
171 let mut data = Vec::with_capacity(discriminator.len() + serialized.len());
172 data.extend_from_slice(discriminator);
173 data.extend_from_slice(&serialized);
174
175 Instruction {
176 program_id: *program_id,
177 accounts,
178 data,
179 }
180}
181
182#[allow(clippy::too_many_arguments)]
184pub fn create_decompress_accounts_idempotent_instruction<T>(
185 program_id: &Pubkey,
186 discriminator: &[u8],
187 hot_addresses: &[Pubkey],
188 cold_accounts: &[(CompressedAccount, T)],
189 program_account_metas: &[AccountMeta],
190 proof: ValidityProofWithContext,
191) -> Result<Instruction, Box<dyn std::error::Error>>
192where
193 T: Pack<solana_instruction::AccountMeta> + Clone + std::fmt::Debug,
194{
195 if cold_accounts.is_empty() {
196 return Err("cold_accounts cannot be empty".into());
197 }
198 if hot_addresses.len() != cold_accounts.len() {
199 return Err("hot_addresses and cold_accounts must have same length".into());
200 }
201
202 let mut remaining_accounts = PackedAccounts::default();
203
204 let mut pda_indices = Vec::new();
206 let mut token_indices = Vec::new();
207 for (i, (acc, _)) in cold_accounts.iter().enumerate() {
208 if acc.owner == LIGHT_TOKEN_PROGRAM_ID {
209 token_indices.push(i);
210 } else {
211 pda_indices.push(i);
212 }
213 }
214 let has_pdas = !pda_indices.is_empty();
215 let has_tokens = !token_indices.is_empty();
216 if !has_tokens && !has_pdas {
217 return Err("No tokens or PDAs found".into());
218 }
219
220 if has_pdas && has_tokens {
222 let first_token_acc = &cold_accounts[token_indices[0]];
223 let first_token_cpi = first_token_acc
224 .0
225 .tree_info
226 .cpi_context
227 .ok_or("missing cpi_context on token account")?;
228 let config = SystemAccountMetaConfig::new_with_cpi_context(*program_id, first_token_cpi);
229 remaining_accounts.add_system_accounts_v2(config)?;
230 } else {
231 remaining_accounts.add_system_accounts_v2(SystemAccountMetaConfig::new(*program_id))?;
232 }
233
234 let output_queue = get_output_queue(&cold_accounts[0].0.tree_info);
235 let output_state_tree_index = remaining_accounts.insert_or_get(output_queue);
236
237 let packed_tree_infos = proof.pack_tree_infos(&mut remaining_accounts);
238 let tree_infos = &packed_tree_infos
239 .state_trees
240 .as_ref()
241 .ok_or("missing state_trees in packed_tree_infos")?
242 .packed_tree_infos;
243
244 let mut accounts = program_account_metas.to_vec();
245 let mut typed_accounts = Vec::with_capacity(cold_accounts.len());
246
247 for &i in pda_indices.iter().chain(token_indices.iter()) {
249 let (acc, data) = &cold_accounts[i];
250 let _queue_index = remaining_accounts.insert_or_get(acc.tree_info.queue);
251 let tree_info = tree_infos
252 .get(i)
253 .copied()
254 .ok_or("tree info index out of bounds")?;
255
256 let packed_data = data.pack(&mut remaining_accounts)?;
257 typed_accounts.push(CompressedAccountData {
258 tree_info,
259 data: packed_data,
260 });
261 }
262
263 let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas();
264 accounts.extend(system_accounts);
265
266 for &i in pda_indices.iter().chain(token_indices.iter()) {
268 accounts.push(AccountMeta::new(hot_addresses[i], false));
269 }
270
271 let full_offset = program_account_metas.len() + system_accounts_offset;
273 let token_accounts_offset = pda_indices.len() as u8;
274 let ix_data = LoadAccountsData {
275 proof: proof.proof,
276 compressed_accounts: typed_accounts,
277 system_accounts_offset: full_offset as u8,
278 token_accounts_offset,
279 output_queue_index: output_state_tree_index,
280 };
281
282 let serialized = ix_data.try_to_vec()?;
283 let mut data = Vec::with_capacity(discriminator.len() + serialized.len());
284 data.extend_from_slice(discriminator);
285 data.extend_from_slice(&serialized);
286
287 Ok(Instruction {
288 program_id: *program_id,
289 accounts,
290 data,
291 })
292}
293
294pub fn build_compress_accounts_idempotent(
296 program_id: &Pubkey,
297 discriminator: &[u8],
298 account_pubkeys: &[Pubkey],
299 program_account_metas: &[AccountMeta],
300 proof: ValidityProofWithContext,
301) -> Result<Instruction, Box<dyn std::error::Error>> {
302 if proof.accounts.is_empty() {
303 return Err("proof.accounts cannot be empty".into());
304 }
305
306 let mut remaining_accounts = PackedAccounts::default();
307 remaining_accounts.add_system_accounts_v2(SystemAccountMetaConfig::new(*program_id))?;
308
309 let output_queue = get_output_queue(&proof.accounts[0].tree_info);
310 let output_state_tree_index = remaining_accounts.insert_or_get(output_queue);
311
312 let packed_tree_infos = proof.pack_tree_infos(&mut remaining_accounts);
313 let tree_infos = packed_tree_infos
314 .state_trees
315 .as_ref()
316 .ok_or("missing state_trees in packed_tree_infos")?;
317
318 let cold_metas: Vec<_> = tree_infos
319 .packed_tree_infos
320 .iter()
321 .map(|tree_info| CompressedAccountMetaNoLamportsNoAddress {
322 tree_info: *tree_info,
323 output_state_tree_index,
324 })
325 .collect();
326
327 let mut accounts = program_account_metas.to_vec();
328 let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas();
329 accounts.extend(system_accounts);
330
331 for pubkey in account_pubkeys {
332 accounts.push(AccountMeta::new(*pubkey, false));
333 }
334
335 let full_offset = program_account_metas.len() + system_accounts_offset;
337 let ix_data = SaveAccountsData {
338 proof: proof.proof,
339 compressed_accounts: cold_metas,
340 system_accounts_offset: full_offset as u8,
341 };
342
343 let serialized = ix_data.try_to_vec()?;
344 let mut data = Vec::with_capacity(discriminator.len() + serialized.len());
345 data.extend_from_slice(discriminator);
346 data.extend_from_slice(&serialized);
347
348 Ok(Instruction {
349 program_id: *program_id,
350 accounts,
351 data,
352 })
353}