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}