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    instruction::system_accounts::{get_light_system_account_metas, SystemAccountMetaConfig},
126    AccountMeta, Pubkey,
127};
128
129/// Builder for organizing accounts into compressed account instructions.
130///
131/// Manages three categories of accounts:
132/// - **Pre-accounts**: Signers and other custom accounts that come before system accounts.
133/// - **System accounts**: Light system program accounts (authority, trees, queues).
134/// - **Packed accounts**: Dynamically tracked deduplicted accounts.
135///
136/// # Example
137///
138/// ```rust
139/// # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig};
140/// # use solana_pubkey::Pubkey;
141/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
142/// # let payer_pubkey = Pubkey::new_unique();
143/// # let program_id = Pubkey::new_unique();
144/// # let merkle_tree_pubkey = Pubkey::new_unique();
145/// let mut accounts = PackedAccounts::default();
146///
147/// // Add signer
148/// accounts.add_pre_accounts_signer(payer_pubkey);
149///
150/// // Add system accounts (use v2 if feature is enabled)
151/// let config = SystemAccountMetaConfig::new(program_id);
152/// #[cfg(feature = "v2")]
153/// accounts.add_system_accounts_v2(config)?;
154/// #[cfg(not(feature = "v2"))]
155/// accounts.add_system_accounts(config)?;
156///
157/// // Add and track tree accounts
158/// let tree_index = accounts.insert_or_get(merkle_tree_pubkey);
159///
160/// // Get final account metas
161/// let (metas, system_offset, tree_offset) = accounts.to_account_metas();
162/// # assert_eq!(tree_index, 0);
163/// # Ok(())
164/// # }
165/// ```
166#[derive(Default, Debug)]
167pub struct PackedAccounts {
168    /// Accounts that must come before system accounts (e.g., signers, fee payer).
169    pub pre_accounts: Vec<AccountMeta>,
170    /// Light system program accounts (authority, programs, trees, queues).
171    system_accounts: Vec<AccountMeta>,
172    /// Next available index for packed accounts.
173    next_index: u8,
174    /// Map of pubkey to (index, AccountMeta) for deduplication and index tracking.
175    map: HashMap<Pubkey, (u8, AccountMeta)>,
176}
177
178impl PackedAccounts {
179    pub fn new_with_system_accounts(config: SystemAccountMetaConfig) -> crate::error::Result<Self> {
180        let mut remaining_accounts = PackedAccounts::default();
181        remaining_accounts.add_system_accounts(config)?;
182        Ok(remaining_accounts)
183    }
184
185    pub fn add_pre_accounts_signer(&mut self, pubkey: Pubkey) {
186        self.pre_accounts.push(AccountMeta {
187            pubkey,
188            is_signer: true,
189            is_writable: false,
190        });
191    }
192
193    pub fn add_pre_accounts_signer_mut(&mut self, pubkey: Pubkey) {
194        self.pre_accounts.push(AccountMeta {
195            pubkey,
196            is_signer: true,
197            is_writable: true,
198        });
199    }
200
201    pub fn add_pre_accounts_meta(&mut self, account_meta: AccountMeta) {
202        self.pre_accounts.push(account_meta);
203    }
204
205    pub fn add_pre_accounts_metas(&mut self, account_metas: &[AccountMeta]) {
206        self.pre_accounts.extend_from_slice(account_metas);
207    }
208
209    /// Adds v1 Light system program accounts to the account list.
210    ///
211    /// **Use with [`cpi::v1::CpiAccounts`](crate::cpi::v1::CpiAccounts) on the program side.**
212    ///
213    /// This adds all the accounts required by the Light system program for v1 operations,
214    /// including the CPI authority, registered programs, account compression program, and Noop program.
215    ///
216    /// # Example
217    ///
218    /// ```rust
219    /// # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig};
220    /// # use solana_pubkey::Pubkey;
221    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
222    /// # let program_id = Pubkey::new_unique();
223    /// let mut accounts = PackedAccounts::default();
224    /// let config = SystemAccountMetaConfig::new(program_id);
225    /// accounts.add_system_accounts(config)?;
226    /// # Ok(())
227    /// # }
228    /// ```
229    pub fn add_system_accounts(
230        &mut self,
231        config: SystemAccountMetaConfig,
232    ) -> crate::error::Result<()> {
233        self.system_accounts
234            .extend(get_light_system_account_metas(config));
235        // note cpi context account is part of the system accounts
236        /*  if let Some(pubkey) = config.cpi_context {
237            if self.next_index != 0 {
238                return Err(crate::error::LightSdkError::CpiContextOrderingViolation);
239            }
240            self.insert_or_get(pubkey);
241        }*/
242        Ok(())
243    }
244
245    /// Adds v2 Light system program accounts to the account list.
246    ///
247    /// **Use with [`cpi::v2::CpiAccounts`](crate::cpi::v2::CpiAccounts) on the program side.**
248    ///
249    /// This adds all the accounts required by the Light system program for v2 operations.
250    /// V2 uses a different account layout optimized for batched state trees.
251    ///
252    /// # Example
253    ///
254    /// ```rust
255    /// # #[cfg(feature = "v2")]
256    /// # {
257    /// # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig};
258    /// # use solana_pubkey::Pubkey;
259    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
260    /// # let program_id = Pubkey::new_unique();
261    /// let mut accounts = PackedAccounts::default();
262    /// let config = SystemAccountMetaConfig::new(program_id);
263    /// accounts.add_system_accounts_v2(config)?;
264    /// # Ok(())
265    /// # }
266    /// # }
267    /// ```
268    #[cfg(feature = "v2")]
269    pub fn add_system_accounts_v2(
270        &mut self,
271        config: SystemAccountMetaConfig,
272    ) -> crate::error::Result<()> {
273        self.system_accounts
274            .extend(crate::instruction::get_light_system_account_metas_v2(
275                config,
276            ));
277        // note cpi context account is part of the system accounts
278        /*  if let Some(pubkey) = config.cpi_context {
279            if self.next_index != 0 {
280                return Err(crate::error::LightSdkError::CpiContextOrderingViolation);
281            }
282            self.insert_or_get(pubkey);
283        }*/
284        Ok(())
285    }
286
287    /// Returns the index of the provided `pubkey` in the collection.
288    ///
289    /// If the provided `pubkey` is not a part of the collection, it gets
290    /// inserted with a `next_index`.
291    ///
292    /// If the privided `pubkey` already exists in the collection, its already
293    /// existing index is returned.
294    pub fn insert_or_get(&mut self, pubkey: Pubkey) -> u8 {
295        self.insert_or_get_config(pubkey, false, true)
296    }
297
298    pub fn insert_or_get_read_only(&mut self, pubkey: Pubkey) -> u8 {
299        self.insert_or_get_config(pubkey, false, false)
300    }
301
302    pub fn insert_or_get_config(
303        &mut self,
304        pubkey: Pubkey,
305        is_signer: bool,
306        is_writable: bool,
307    ) -> u8 {
308        match self.map.get_mut(&pubkey) {
309            Some((index, entry)) => {
310                if !entry.is_writable {
311                    entry.is_writable = is_writable;
312                }
313                if !entry.is_signer {
314                    entry.is_signer = is_signer;
315                }
316                *index
317            }
318            None => {
319                let index = self.next_index;
320                self.next_index += 1;
321                self.map.insert(
322                    pubkey,
323                    (
324                        index,
325                        AccountMeta {
326                            pubkey,
327                            is_signer,
328                            is_writable,
329                        },
330                    ),
331                );
332                index
333            }
334        }
335    }
336
337    fn hash_set_accounts_to_metas(&self) -> Vec<AccountMeta> {
338        let mut packed_accounts = self.map.iter().collect::<Vec<_>>();
339        // hash maps are not sorted so we need to sort manually and collect into a vector again
340        packed_accounts.sort_by(|a, b| a.1 .0.cmp(&b.1 .0));
341        let packed_accounts = packed_accounts
342            .iter()
343            .map(|(_, (_, k))| k.clone())
344            .collect::<Vec<AccountMeta>>();
345        packed_accounts
346    }
347
348    fn get_offsets(&self) -> (usize, usize) {
349        let system_accounts_start_offset = self.pre_accounts.len();
350        let packed_accounts_start_offset =
351            system_accounts_start_offset + self.system_accounts.len();
352        (system_accounts_start_offset, packed_accounts_start_offset)
353    }
354
355    /// Converts the collection of accounts to a vector of
356    /// [`AccountMeta`](solana_instruction::AccountMeta), which can be used
357    /// as remaining accounts in instructions or CPI calls.
358    ///
359    /// # Returns
360    ///
361    /// A tuple of `(account_metas, system_accounts_offset, packed_accounts_offset)`:
362    /// - `account_metas`: All accounts concatenated in order: `[pre_accounts][system_accounts][packed_accounts]`
363    /// - `system_accounts_offset`: Index where system accounts start (= pre_accounts.len())
364    /// - `packed_accounts_offset`: Index where packed accounts start (= pre_accounts.len() + system_accounts.len())
365    ///
366    /// The `system_accounts_offset` can be used to slice the accounts when creating [`CpiAccounts`](crate::cpi::v1::CpiAccounts):
367    /// ```ignore
368    /// let accounts_for_cpi = &ctx.remaining_accounts[system_accounts_offset..];
369    /// let cpi_accounts = CpiAccounts::new(fee_payer, accounts_for_cpi, cpi_signer)?;
370    /// ```
371    ///
372    /// The offset can be hardcoded if your program always has the same pre-accounts layout, or passed
373    /// as a field in your instruction data.
374    pub fn to_account_metas(&self) -> (Vec<AccountMeta>, usize, usize) {
375        let packed_accounts = self.hash_set_accounts_to_metas();
376        let (system_accounts_start_offset, packed_accounts_start_offset) = self.get_offsets();
377        (
378            [
379                self.pre_accounts.clone(),
380                self.system_accounts.clone(),
381                packed_accounts,
382            ]
383            .concat(),
384            system_accounts_start_offset,
385            packed_accounts_start_offset,
386        )
387    }
388
389    pub fn packed_pubkeys(&self) -> Vec<Pubkey> {
390        self.hash_set_accounts_to_metas()
391            .iter()
392            .map(|meta| meta.pubkey)
393            .collect()
394    }
395}
396
397#[cfg(test)]
398mod test {
399    use super::*;
400
401    #[test]
402    fn test_remaining_accounts() {
403        let mut remaining_accounts = PackedAccounts::default();
404
405        let pubkey_1 = Pubkey::new_unique();
406        let pubkey_2 = Pubkey::new_unique();
407        let pubkey_3 = Pubkey::new_unique();
408        let pubkey_4 = Pubkey::new_unique();
409
410        // Initial insertion.
411        assert_eq!(remaining_accounts.insert_or_get(pubkey_1), 0);
412        assert_eq!(remaining_accounts.insert_or_get(pubkey_2), 1);
413        assert_eq!(remaining_accounts.insert_or_get(pubkey_3), 2);
414
415        assert_eq!(
416            remaining_accounts.to_account_metas().0.as_slice(),
417            &[
418                AccountMeta {
419                    pubkey: pubkey_1,
420                    is_signer: false,
421                    is_writable: true,
422                },
423                AccountMeta {
424                    pubkey: pubkey_2,
425                    is_signer: false,
426                    is_writable: true,
427                },
428                AccountMeta {
429                    pubkey: pubkey_3,
430                    is_signer: false,
431                    is_writable: true,
432                }
433            ]
434        );
435
436        // Insertion of already existing pubkeys.
437        assert_eq!(remaining_accounts.insert_or_get(pubkey_1), 0);
438        assert_eq!(remaining_accounts.insert_or_get(pubkey_2), 1);
439        assert_eq!(remaining_accounts.insert_or_get(pubkey_3), 2);
440
441        assert_eq!(
442            remaining_accounts.to_account_metas().0.as_slice(),
443            &[
444                AccountMeta {
445                    pubkey: pubkey_1,
446                    is_signer: false,
447                    is_writable: true,
448                },
449                AccountMeta {
450                    pubkey: pubkey_2,
451                    is_signer: false,
452                    is_writable: true,
453                },
454                AccountMeta {
455                    pubkey: pubkey_3,
456                    is_signer: false,
457                    is_writable: true,
458                }
459            ]
460        );
461
462        // Again, initial insertion.
463        assert_eq!(remaining_accounts.insert_or_get(pubkey_4), 3);
464
465        assert_eq!(
466            remaining_accounts.to_account_metas().0.as_slice(),
467            &[
468                AccountMeta {
469                    pubkey: pubkey_1,
470                    is_signer: false,
471                    is_writable: true,
472                },
473                AccountMeta {
474                    pubkey: pubkey_2,
475                    is_signer: false,
476                    is_writable: true,
477                },
478                AccountMeta {
479                    pubkey: pubkey_3,
480                    is_signer: false,
481                    is_writable: true,
482                },
483                AccountMeta {
484                    pubkey: pubkey_4,
485                    is_signer: false,
486                    is_writable: true,
487                }
488            ]
489        );
490    }
491}