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}