light_sdk/instruction/pack_accounts.rs
1//! Utilities for packing accounts into instruction data.
2//!
3//! [`PackedAccounts`] is a builder for efficiently organizing accounts into the three categories
4//! required for compressed account instructions:
5//! 1. **Pre-accounts** - Custom accounts needed before system accounts
6//! 2. **System accounts** - Static light system program accounts
7//! 3. **Packed accounts** - Dynamically packed accounts (Merkle trees, address trees, queues) with automatic deduplication
8//!
9//!
10//! ## System Account Versioning
11//!
12//! **`add_system_accounts()` is complementary to [`cpi::v1::CpiAccounts`](crate::cpi::v1::CpiAccounts)**
13//! **`add_system_accounts_v2()` is complementary to [`cpi::v2::CpiAccounts`](crate::cpi::v2::CpiAccounts)**
14//!
15//! Always use the matching version - v1 client-side account packing with v1 program-side CPI,
16//! and v2 with v2. Mixing versions will cause account layout mismatches.
17//!
18//! # Example: Creating a compressed PDA
19//!
20//! ```rust
21//! # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig};
22//! # use solana_pubkey::Pubkey;
23//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
24//! # let program_id = Pubkey::new_unique();
25//! # let payer_pubkey = Pubkey::new_unique();
26//! # let merkle_tree_pubkey = Pubkey::new_unique();
27//! // Initialize with system accounts
28//! let system_account_meta_config = SystemAccountMetaConfig::new(program_id);
29//! let mut accounts = PackedAccounts::default();
30//!
31//! // Add pre-accounts (signers)
32//! accounts.add_pre_accounts_signer(payer_pubkey);
33//!
34//! // Add Light system program accounts (v2)
35//! #[cfg(feature = "v2")]
36//! accounts.add_system_accounts_v2(system_account_meta_config)?;
37//! #[cfg(not(feature = "v2"))]
38//! accounts.add_system_accounts(system_account_meta_config)?;
39//!
40//! // Add Merkle tree accounts (automatically tracked and deduplicated)
41//! let output_merkle_tree_index = accounts.insert_or_get(merkle_tree_pubkey);
42//!
43//! // Convert to final account metas with offsets
44//! let (account_metas, system_accounts_offset, tree_accounts_offset) = accounts.to_account_metas();
45//! # assert_eq!(output_merkle_tree_index, 0);
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! # Account Organization
51//!
52//! The final account layout is:
53//! ```text
54//! [pre_accounts] [system_accounts] [packed_accounts]
55//! ↑ ↑ ↑
56//! Signers, Light system Merkle trees,
57//! fee payer program accts address trees
58//! ```
59//!
60//! # Automatic Deduplication
61//!
62//! ```rust
63//! # use light_sdk::instruction::PackedAccounts;
64//! # use solana_pubkey::Pubkey;
65//! let mut accounts = PackedAccounts::default();
66//! let tree_pubkey = Pubkey::new_unique();
67//! let other_tree = Pubkey::new_unique();
68//!
69//! // First insertion gets index 0
70//! let index1 = accounts.insert_or_get(tree_pubkey);
71//! assert_eq!(index1, 0);
72//!
73//! // Same tree inserted again returns same index (deduplicated)
74//! let index2 = accounts.insert_or_get(tree_pubkey);
75//! assert_eq!(index2, 0);
76//!
77//! // Different tree gets next index
78//! let index3 = accounts.insert_or_get(other_tree);
79//! assert_eq!(index3, 1);
80//! ```
81//!
82//! # Building Instructions with Anchor Programs
83//!
84//! When building instructions for Anchor programs, concatenate your custom accounts with the packed accounts:
85//!
86//! ```rust,ignore
87//! # use anchor_lang::InstructionData;
88//! # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig};
89//! # use solana_instruction::{AccountMeta, Instruction};
90//!
91//! // 1. Set up packed accounts
92//! let config = SystemAccountMetaConfig::new(program_id);
93//! let mut remaining_accounts = PackedAccounts::default();
94//! remaining_accounts.add_system_accounts(config)?;
95//!
96//! // 2. Pack tree accounts from proof result
97//! let packed_tree_info = proof_result.pack_tree_infos(&mut remaining_accounts);
98//! let output_tree_index = state_tree_info.pack_output_tree_index(&mut remaining_accounts)?;
99//!
100//! // 3. Convert to account metas
101//! let (remaining_accounts, _, _) = remaining_accounts.to_account_metas();
102//!
103//! // 4. Build instruction: custom accounts first, then remaining_accounts
104//! let instruction = Instruction {
105//! program_id: your_program::ID,
106//! accounts: [
107//! vec![AccountMeta::new(payer.pubkey(), true)], // Your program's accounts
108//! // Add other custom accounts here if needed
109//! remaining_accounts, // Light system accounts + trees
110//! ]
111//! .concat(),
112//! data: your_program::instruction::YourInstruction {
113//! proof: proof_result.proof,
114//! address_tree_info: packed_tree_info.address_trees[0],
115//! output_tree_index,
116//! // ... your other fields
117//! }
118//! .data(),
119//! };
120//! ```
121
122use std::collections::HashMap;
123
124use crate::{
125 error::LightSdkError,
126 instruction::system_accounts::{get_light_system_account_metas, SystemAccountMetaConfig},
127 AccountMeta, Pubkey,
128};
129
130/// Builder for organizing accounts into compressed account instructions.
131///
132/// Manages three categories of accounts:
133/// - **Pre-accounts**: Signers and other custom accounts that come before system accounts.
134/// - **System accounts**: Light system program accounts (authority, trees, queues).
135/// - **Packed accounts**: Dynamically tracked deduplicted accounts.
136///
137/// # Example
138///
139/// ```rust
140/// # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig};
141/// # use solana_pubkey::Pubkey;
142/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
143/// # let payer_pubkey = Pubkey::new_unique();
144/// # let program_id = Pubkey::new_unique();
145/// # let merkle_tree_pubkey = Pubkey::new_unique();
146/// let mut accounts = PackedAccounts::default();
147///
148/// // Add signer
149/// accounts.add_pre_accounts_signer(payer_pubkey);
150///
151/// // Add system accounts (use v2 if feature is enabled)
152/// let config = SystemAccountMetaConfig::new(program_id);
153/// #[cfg(feature = "v2")]
154/// accounts.add_system_accounts_v2(config)?;
155/// #[cfg(not(feature = "v2"))]
156/// accounts.add_system_accounts(config)?;
157///
158/// // Add and track tree accounts
159/// let tree_index = accounts.insert_or_get(merkle_tree_pubkey);
160///
161/// // Get final account metas
162/// let (metas, system_offset, tree_offset) = accounts.to_account_metas();
163/// # assert_eq!(tree_index, 0);
164/// # Ok(())
165/// # }
166/// ```
167#[derive(Default, Debug)]
168pub struct PackedAccounts {
169 /// Accounts that must come before system accounts (e.g., signers, fee payer).
170 pub pre_accounts: Vec<AccountMeta>,
171 /// Light system program accounts (authority, programs, trees, queues).
172 system_accounts: Vec<AccountMeta>,
173 /// Next available index for packed accounts.
174 next_index: u8,
175 /// Map of pubkey to (index, AccountMeta) for deduplication and index tracking.
176 map: HashMap<Pubkey, (u8, AccountMeta)>,
177 /// Field to sanity check
178 system_accounts_set: bool,
179}
180
181impl PackedAccounts {
182 pub fn new_with_system_accounts(config: SystemAccountMetaConfig) -> crate::error::Result<Self> {
183 let mut remaining_accounts = PackedAccounts::default();
184 remaining_accounts.add_system_accounts(config)?;
185 Ok(remaining_accounts)
186 }
187
188 pub fn system_accounts_set(&self) -> bool {
189 self.system_accounts_set
190 }
191
192 pub fn add_pre_accounts_signer(&mut self, pubkey: Pubkey) {
193 self.pre_accounts.push(AccountMeta {
194 pubkey,
195 is_signer: true,
196 is_writable: false,
197 });
198 }
199
200 pub fn add_pre_accounts_signer_mut(&mut self, pubkey: Pubkey) {
201 self.pre_accounts.push(AccountMeta {
202 pubkey,
203 is_signer: true,
204 is_writable: true,
205 });
206 }
207
208 pub fn add_pre_accounts_meta(&mut self, account_meta: AccountMeta) {
209 self.pre_accounts.push(account_meta);
210 }
211
212 pub fn add_pre_accounts_metas(&mut self, account_metas: &[AccountMeta]) {
213 self.pre_accounts.extend_from_slice(account_metas);
214 }
215
216 /// Adds v1 Light system program accounts to the account list.
217 ///
218 /// **Use with [`cpi::v1::CpiAccounts`](crate::cpi::v1::CpiAccounts) on the program side.**
219 ///
220 /// This adds all the accounts required by the Light system program for v1 operations,
221 /// including the CPI authority, registered programs, account compression program, and Noop program.
222 ///
223 /// # Example
224 ///
225 /// ```rust
226 /// # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig};
227 /// # use solana_pubkey::Pubkey;
228 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
229 /// # let program_id = Pubkey::new_unique();
230 /// let mut accounts = PackedAccounts::default();
231 /// let config = SystemAccountMetaConfig::new(program_id);
232 /// accounts.add_system_accounts(config)?;
233 /// # Ok(())
234 /// # }
235 /// ```
236 pub fn add_system_accounts(
237 &mut self,
238 config: SystemAccountMetaConfig,
239 ) -> crate::error::Result<()> {
240 self.system_accounts
241 .extend(get_light_system_account_metas(config));
242 // note cpi context account is part of the system accounts
243 /* if let Some(pubkey) = config.cpi_context {
244 if self.next_index != 0 {
245 return Err(crate::error::LightSdkError::CpiContextOrderingViolation);
246 }
247 self.insert_or_get(pubkey);
248 }*/
249 Ok(())
250 }
251
252 /// Adds v2 Light system program accounts to the account list.
253 ///
254 /// **Use with [`cpi::v2::CpiAccounts`](crate::cpi::v2::CpiAccounts) on the program side.**
255 ///
256 /// This adds all the accounts required by the Light system program for v2 operations.
257 /// V2 uses a different account layout optimized for batched state trees.
258 ///
259 /// # Example
260 ///
261 /// ```rust
262 /// # #[cfg(feature = "v2")]
263 /// # {
264 /// # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig};
265 /// # use solana_pubkey::Pubkey;
266 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
267 /// # let program_id = Pubkey::new_unique();
268 /// let mut accounts = PackedAccounts::default();
269 /// let config = SystemAccountMetaConfig::new(program_id);
270 /// accounts.add_system_accounts_v2(config)?;
271 /// # Ok(())
272 /// # }
273 /// # }
274 /// ```
275 #[cfg(feature = "v2")]
276 pub fn add_system_accounts_v2(
277 &mut self,
278 config: SystemAccountMetaConfig,
279 ) -> crate::error::Result<()> {
280 self.system_accounts
281 .extend(crate::instruction::get_light_system_account_metas_v2(
282 config,
283 ));
284 // note cpi context account is part of the system accounts
285 /* if let Some(pubkey) = config.cpi_context {
286 if self.next_index != 0 {
287 return Err(crate::error::LightSdkError::CpiContextOrderingViolation);
288 }
289 self.insert_or_get(pubkey);
290 }*/
291 Ok(())
292 }
293
294 /// Returns the index of the provided `pubkey` in the collection.
295 ///
296 /// If the provided `pubkey` is not a part of the collection, it gets
297 /// inserted with a `next_index`.
298 ///
299 /// If the privided `pubkey` already exists in the collection, its already
300 /// existing index is returned.
301 pub fn insert_or_get(&mut self, pubkey: Pubkey) -> u8 {
302 self.insert_or_get_config(pubkey, false, true)
303 }
304
305 pub fn insert_or_get_read_only(&mut self, pubkey: Pubkey) -> u8 {
306 self.insert_or_get_config(pubkey, false, false)
307 }
308
309 pub fn insert_or_get_config(
310 &mut self,
311 pubkey: Pubkey,
312 is_signer: bool,
313 is_writable: bool,
314 ) -> u8 {
315 match self.map.get_mut(&pubkey) {
316 Some((index, entry)) => {
317 if !entry.is_writable {
318 entry.is_writable = is_writable;
319 }
320 if !entry.is_signer {
321 entry.is_signer = is_signer;
322 }
323 *index
324 }
325 None => {
326 let index = self.next_index;
327 self.next_index += 1;
328 self.map.insert(
329 pubkey,
330 (
331 index,
332 AccountMeta {
333 pubkey,
334 is_signer,
335 is_writable,
336 },
337 ),
338 );
339 index
340 }
341 }
342 }
343
344 fn hash_set_accounts_to_metas(&self) -> Vec<AccountMeta> {
345 let mut packed_accounts = self.map.iter().collect::<Vec<_>>();
346 // hash maps are not sorted so we need to sort manually and collect into a vector again
347 packed_accounts.sort_by(|a, b| a.1 .0.cmp(&b.1 .0));
348 let packed_accounts = packed_accounts
349 .iter()
350 .map(|(_, (_, k))| k.clone())
351 .collect::<Vec<AccountMeta>>();
352 packed_accounts
353 }
354
355 fn get_offsets(&self) -> (usize, usize) {
356 let system_accounts_start_offset = self.pre_accounts.len();
357 let packed_accounts_start_offset =
358 system_accounts_start_offset + self.system_accounts.len();
359 (system_accounts_start_offset, packed_accounts_start_offset)
360 }
361
362 /// Converts the collection of accounts to a vector of
363 /// [`AccountMeta`](solana_instruction::AccountMeta), which can be used
364 /// as remaining accounts in instructions or CPI calls.
365 ///
366 /// # Returns
367 ///
368 /// A tuple of `(account_metas, system_accounts_offset, packed_accounts_offset)`:
369 /// - `account_metas`: All accounts concatenated in order: `[pre_accounts][system_accounts][packed_accounts]`
370 /// - `system_accounts_offset`: Index where system accounts start (= pre_accounts.len())
371 /// - `packed_accounts_offset`: Index where packed accounts start (= pre_accounts.len() + system_accounts.len())
372 ///
373 /// The `system_accounts_offset` can be used to slice the accounts when creating [`CpiAccounts`](crate::cpi::v1::CpiAccounts):
374 /// ```ignore
375 /// let accounts_for_cpi = &ctx.remaining_accounts[system_accounts_offset..];
376 /// let cpi_accounts = CpiAccounts::new(fee_payer, accounts_for_cpi, cpi_signer)?;
377 /// ```
378 ///
379 /// The offset can be hardcoded if your program always has the same pre-accounts layout, or passed
380 /// as a field in your instruction data.
381 pub fn to_account_metas(&self) -> (Vec<AccountMeta>, usize, usize) {
382 let packed_accounts = self.hash_set_accounts_to_metas();
383 let (system_accounts_start_offset, packed_accounts_start_offset) = self.get_offsets();
384 (
385 [
386 self.pre_accounts.clone(),
387 self.system_accounts.clone(),
388 packed_accounts,
389 ]
390 .concat(),
391 system_accounts_start_offset,
392 packed_accounts_start_offset,
393 )
394 }
395
396 pub fn packed_pubkeys(&self) -> Vec<Pubkey> {
397 self.hash_set_accounts_to_metas()
398 .iter()
399 .map(|meta| meta.pubkey)
400 .collect()
401 }
402
403 pub fn add_custom_system_accounts<T: AccountMetasVec>(
404 &mut self,
405 accounts: T,
406 ) -> crate::error::Result<()> {
407 accounts.get_account_metas_vec(self)
408 }
409}
410
411pub trait AccountMetasVec {
412 fn get_account_metas_vec(&self, accounts: &mut PackedAccounts) -> Result<(), LightSdkError>;
413}
414
415#[cfg(test)]
416mod test {
417 use super::*;
418
419 #[test]
420 fn test_remaining_accounts() {
421 let mut remaining_accounts = PackedAccounts::default();
422
423 let pubkey_1 = Pubkey::new_unique();
424 let pubkey_2 = Pubkey::new_unique();
425 let pubkey_3 = Pubkey::new_unique();
426 let pubkey_4 = Pubkey::new_unique();
427
428 // Initial insertion.
429 assert_eq!(remaining_accounts.insert_or_get(pubkey_1), 0);
430 assert_eq!(remaining_accounts.insert_or_get(pubkey_2), 1);
431 assert_eq!(remaining_accounts.insert_or_get(pubkey_3), 2);
432
433 assert_eq!(
434 remaining_accounts.to_account_metas().0.as_slice(),
435 &[
436 AccountMeta {
437 pubkey: pubkey_1,
438 is_signer: false,
439 is_writable: true,
440 },
441 AccountMeta {
442 pubkey: pubkey_2,
443 is_signer: false,
444 is_writable: true,
445 },
446 AccountMeta {
447 pubkey: pubkey_3,
448 is_signer: false,
449 is_writable: true,
450 }
451 ]
452 );
453
454 // Insertion of already existing pubkeys.
455 assert_eq!(remaining_accounts.insert_or_get(pubkey_1), 0);
456 assert_eq!(remaining_accounts.insert_or_get(pubkey_2), 1);
457 assert_eq!(remaining_accounts.insert_or_get(pubkey_3), 2);
458
459 assert_eq!(
460 remaining_accounts.to_account_metas().0.as_slice(),
461 &[
462 AccountMeta {
463 pubkey: pubkey_1,
464 is_signer: false,
465 is_writable: true,
466 },
467 AccountMeta {
468 pubkey: pubkey_2,
469 is_signer: false,
470 is_writable: true,
471 },
472 AccountMeta {
473 pubkey: pubkey_3,
474 is_signer: false,
475 is_writable: true,
476 }
477 ]
478 );
479
480 // Again, initial insertion.
481 assert_eq!(remaining_accounts.insert_or_get(pubkey_4), 3);
482
483 assert_eq!(
484 remaining_accounts.to_account_metas().0.as_slice(),
485 &[
486 AccountMeta {
487 pubkey: pubkey_1,
488 is_signer: false,
489 is_writable: true,
490 },
491 AccountMeta {
492 pubkey: pubkey_2,
493 is_signer: false,
494 is_writable: true,
495 },
496 AccountMeta {
497 pubkey: pubkey_3,
498 is_signer: false,
499 is_writable: true,
500 },
501 AccountMeta {
502 pubkey: pubkey_4,
503 is_signer: false,
504 is_writable: true,
505 }
506 ]
507 );
508 }
509}