light_instruction_decoder/programs/
light_system.rs

1//! Light System Program instruction decoder.
2//!
3//! This module provides a macro-derived decoder for the Light System Program,
4//! which uses 8-byte discriminators for compressed account operations.
5//!
6//! ## Instructions
7//!
8//! - `Invoke`: Direct invocation of Light System (has 4-byte Anchor prefix after discriminator)
9//! - `InvokeCpi`: CPI invocation from another program (has 4-byte Anchor prefix after discriminator)
10//! - `InvokeCpiWithReadOnly`: CPI with read-only accounts (no prefix, borsh-only)
11//! - `InvokeCpiWithAccountInfo`: CPI with full account info (no prefix, borsh-only)
12
13// Allow the macro-generated code to reference types from this crate
14extern crate self as light_instruction_decoder;
15
16use borsh::BorshDeserialize;
17use light_compressed_account::instruction_data::{
18    data::InstructionDataInvoke, invoke_cpi::InstructionDataInvokeCpi,
19    with_account_info::InstructionDataInvokeCpiWithAccountInfo,
20    with_readonly::InstructionDataInvokeCpiWithReadOnly,
21};
22use light_instruction_decoder_derive::InstructionDecoder;
23use solana_instruction::AccountMeta;
24use solana_pubkey::Pubkey;
25
26/// System program ID string for account resolution
27const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111";
28
29// ============================================================================
30// Helper Functions for Deduplicating Formatter Code
31// ============================================================================
32
33/// Format input compressed accounts section for Invoke/InvokeCpi.
34///
35/// Formats `PackedCompressedAccountWithMerkleContext` accounts with:
36/// owner, address, lamports, data_hash, discriminator, merkle_tree, leaf_index, root_index
37#[cfg(not(target_os = "solana"))]
38fn format_input_accounts_section(
39    output: &mut String,
40    accounts: &[light_compressed_account::compressed_account::PackedCompressedAccountWithMerkleContext],
41    instruction_accounts: &[AccountMeta],
42) {
43    use std::fmt::Write;
44
45    if accounts.is_empty() {
46        return;
47    }
48
49    let _ = writeln!(output, "Input Accounts ({}):", accounts.len());
50    for (i, acc) in accounts.iter().enumerate() {
51        let _ = writeln!(output, "  [{}]", i);
52        let _ = writeln!(
53            output,
54            "      owner: {}",
55            Pubkey::new_from_array(acc.compressed_account.owner.to_bytes())
56        );
57        if let Some(addr) = acc.compressed_account.address {
58            let _ = writeln!(output, "      address: {:?}", addr);
59        }
60        let _ = writeln!(
61            output,
62            "      lamports: {}",
63            acc.compressed_account.lamports
64        );
65        if let Some(ref acc_data) = acc.compressed_account.data {
66            let _ = writeln!(output, "      data_hash: {:?}", acc_data.data_hash);
67            let _ = writeln!(output, "      discriminator: {:?}", acc_data.discriminator);
68        }
69        let tree_idx = Some(acc.merkle_context.merkle_tree_pubkey_index);
70        let queue_idx = Some(acc.merkle_context.queue_pubkey_index);
71        let (tree_pubkey, _queue_pubkey) =
72            resolve_tree_and_queue_pubkeys(instruction_accounts, tree_idx, queue_idx);
73        if let Some(tp) = tree_pubkey {
74            let _ = writeln!(
75                output,
76                "      merkle_tree_pubkey (index {}): {}",
77                acc.merkle_context.merkle_tree_pubkey_index, tp
78            );
79        }
80        let _ = writeln!(
81            output,
82            "      leaf_index: {}",
83            acc.merkle_context.leaf_index
84        );
85        let _ = writeln!(output, "      root_index: {}", acc.root_index);
86    }
87}
88
89/// Format input compressed accounts section for InvokeCpiWithReadOnly.
90///
91/// Formats `InAccount` accounts with a shared owner from `invoking_program_id`.
92#[cfg(not(target_os = "solana"))]
93fn format_readonly_input_accounts_section(
94    output: &mut String,
95    accounts: &[light_compressed_account::instruction_data::with_readonly::InAccount],
96    invoking_program_id: &light_compressed_account::pubkey::Pubkey,
97    instruction_accounts: &[AccountMeta],
98) {
99    use std::fmt::Write;
100
101    if accounts.is_empty() {
102        return;
103    }
104
105    let _ = writeln!(output, "Input Accounts ({}):", accounts.len());
106    for (i, acc) in accounts.iter().enumerate() {
107        let _ = writeln!(output, "  [{}]", i);
108        let _ = writeln!(
109            output,
110            "      owner: {}",
111            Pubkey::new_from_array(invoking_program_id.to_bytes())
112        );
113        if let Some(addr) = acc.address {
114            let _ = writeln!(output, "      address: {:?}", addr);
115        }
116        let _ = writeln!(output, "      lamports: {}", acc.lamports);
117        let _ = writeln!(output, "      data_hash: {:?}", acc.data_hash);
118        let _ = writeln!(output, "      discriminator: {:?}", acc.discriminator);
119        let tree_idx = Some(acc.merkle_context.merkle_tree_pubkey_index);
120        let queue_idx = Some(acc.merkle_context.queue_pubkey_index);
121        let (tree_pubkey, _queue_pubkey) =
122            resolve_tree_and_queue_pubkeys(instruction_accounts, tree_idx, queue_idx);
123        if let Some(tp) = tree_pubkey {
124            let _ = writeln!(
125                output,
126                "      merkle_tree_pubkey (index {}): {}",
127                acc.merkle_context.merkle_tree_pubkey_index, tp
128            );
129        }
130        let _ = writeln!(
131            output,
132            "      leaf_index: {}",
133            acc.merkle_context.leaf_index
134        );
135        let _ = writeln!(output, "      root_index: {}", acc.root_index);
136    }
137}
138
139/// Format output compressed accounts section.
140///
141/// Formats `OutputCompressedAccountWithPackedContext` accounts with:
142/// owner, address, lamports, data_hash, discriminator, data, merkle_tree
143#[cfg(not(target_os = "solana"))]
144fn format_output_accounts_section(
145    output: &mut String,
146    accounts: &[light_compressed_account::instruction_data::data::OutputCompressedAccountWithPackedContext],
147    instruction_accounts: &[AccountMeta],
148) {
149    use std::fmt::Write;
150
151    if accounts.is_empty() {
152        return;
153    }
154
155    let _ = writeln!(output, "Output Accounts ({}):", accounts.len());
156    for (i, acc) in accounts.iter().enumerate() {
157        let _ = writeln!(output, "  [{}]", i);
158        let _ = writeln!(
159            output,
160            "      owner: {}",
161            Pubkey::new_from_array(acc.compressed_account.owner.to_bytes())
162        );
163        if let Some(addr) = acc.compressed_account.address {
164            let _ = writeln!(output, "      address: {:?}", addr);
165        }
166        let _ = writeln!(
167            output,
168            "      lamports: {}",
169            acc.compressed_account.lamports
170        );
171        if let Some(ref acc_data) = acc.compressed_account.data {
172            let _ = writeln!(output, "      data_hash: {:?}", acc_data.data_hash);
173            let _ = writeln!(output, "      discriminator: {:?}", acc_data.discriminator);
174            let _ = writeln!(
175                output,
176                "      data ({} bytes): {:?}",
177                acc_data.data.len(),
178                acc_data.data
179            );
180        }
181        let tree_idx = Some(acc.merkle_tree_index);
182        let (tree_pubkey, _) = resolve_tree_and_queue_pubkeys(instruction_accounts, tree_idx, None);
183        if let Some(tp) = tree_pubkey {
184            let _ = writeln!(
185                output,
186                "      merkle_tree_pubkey (index {}): {}",
187                acc.merkle_tree_index, tp
188            );
189        }
190    }
191}
192
193/// Format output compressed accounts section for InvokeCpiWithReadOnly.
194///
195/// Uses `invoking_program_id` as owner instead of per-account owner.
196#[cfg(not(target_os = "solana"))]
197fn format_readonly_output_accounts_section(
198    output: &mut String,
199    accounts: &[light_compressed_account::instruction_data::data::OutputCompressedAccountWithPackedContext],
200    invoking_program_id: &light_compressed_account::pubkey::Pubkey,
201    instruction_accounts: &[AccountMeta],
202) {
203    use std::fmt::Write;
204
205    if accounts.is_empty() {
206        return;
207    }
208
209    let _ = writeln!(output, "Output Accounts ({}):", accounts.len());
210    for (i, acc) in accounts.iter().enumerate() {
211        let _ = writeln!(output, "  [{}]", i);
212        let _ = writeln!(
213            output,
214            "      owner: {}",
215            Pubkey::new_from_array(invoking_program_id.to_bytes())
216        );
217        if let Some(addr) = acc.compressed_account.address {
218            let _ = writeln!(output, "      address: {:?}", addr);
219        }
220        let _ = writeln!(
221            output,
222            "      lamports: {}",
223            acc.compressed_account.lamports
224        );
225        if let Some(ref acc_data) = acc.compressed_account.data {
226            let _ = writeln!(output, "      data_hash: {:?}", acc_data.data_hash);
227            let _ = writeln!(output, "      discriminator: {:?}", acc_data.discriminator);
228            let _ = writeln!(
229                output,
230                "      data ({} bytes): {:?}",
231                acc_data.data.len(),
232                acc_data.data
233            );
234        }
235        let tree_idx = Some(acc.merkle_tree_index);
236        let (tree_pubkey, _) = resolve_tree_and_queue_pubkeys(instruction_accounts, tree_idx, None);
237        if let Some(tp) = tree_pubkey {
238            let _ = writeln!(
239                output,
240                "      merkle_tree_pubkey (index {}): {}",
241                acc.merkle_tree_index, tp
242            );
243        }
244    }
245}
246
247/// Format new address params section for Invoke/InvokeCpi.
248///
249/// Formats `NewAddressParamsPacked` with: seed, queue, tree
250#[cfg(not(target_os = "solana"))]
251fn format_new_address_params_section(
252    output: &mut String,
253    params: &[light_compressed_account::instruction_data::data::NewAddressParamsPacked],
254    instruction_accounts: &[AccountMeta],
255) {
256    use std::fmt::Write;
257
258    if params.is_empty() {
259        return;
260    }
261
262    let _ = writeln!(output, "New Addresses ({}):", params.len());
263    for (i, param) in params.iter().enumerate() {
264        let _ = writeln!(output, "  [{}] seed: {:?}", i, param.seed);
265        let tree_idx = Some(param.address_merkle_tree_account_index);
266        let queue_idx = Some(param.address_queue_account_index);
267        let (tree_pubkey, queue_pubkey) =
268            resolve_tree_and_queue_pubkeys(instruction_accounts, tree_idx, queue_idx);
269        if let Some(qp) = queue_pubkey {
270            let _ = writeln!(
271                output,
272                "      queue[{}]: {}",
273                param.address_queue_account_index, qp
274            );
275        }
276        if let Some(tp) = tree_pubkey {
277            let _ = writeln!(
278                output,
279                "      tree[{}]: {}",
280                param.address_merkle_tree_account_index, tp
281            );
282        }
283    }
284}
285
286/// Format new address params section with assignment info.
287///
288/// Formats `NewAddressParamsAssignedPacked` with: seed, queue, tree, assigned
289#[cfg(not(target_os = "solana"))]
290fn format_new_address_params_assigned_section(
291    output: &mut String,
292    params: &[light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked],
293    instruction_accounts: &[AccountMeta],
294) {
295    use std::fmt::Write;
296
297    if params.is_empty() {
298        return;
299    }
300
301    let _ = writeln!(output, "New Addresses ({}):", params.len());
302    for (i, param) in params.iter().enumerate() {
303        let _ = writeln!(output, "  [{}] seed: {:?}", i, param.seed);
304        let tree_idx = Some(param.address_merkle_tree_account_index);
305        let queue_idx = Some(param.address_queue_account_index);
306        let (tree_pubkey, queue_pubkey) =
307            resolve_tree_and_queue_pubkeys(instruction_accounts, tree_idx, queue_idx);
308        if let Some(qp) = queue_pubkey {
309            let _ = writeln!(
310                output,
311                "      queue[{}]: {}",
312                param.address_queue_account_index, qp
313            );
314        }
315        if let Some(tp) = tree_pubkey {
316            let _ = writeln!(
317                output,
318                "      tree[{}]: {}",
319                param.address_merkle_tree_account_index, tp
320            );
321        }
322        let assigned = if param.assigned_to_account {
323            format!("account[{}]", param.assigned_account_index)
324        } else {
325            "None".to_string()
326        };
327        let _ = writeln!(output, "      assigned: {}", assigned);
328    }
329}
330
331/// Format read-only addresses section.
332///
333/// Formats `PackedReadOnlyAddress` with: address, tree
334#[cfg(not(target_os = "solana"))]
335fn format_read_only_addresses_section(
336    output: &mut String,
337    addresses: &[light_compressed_account::instruction_data::data::PackedReadOnlyAddress],
338    instruction_accounts: &[AccountMeta],
339) {
340    use std::fmt::Write;
341
342    if addresses.is_empty() {
343        return;
344    }
345
346    let _ = writeln!(output, "Read-Only Addresses ({}):", addresses.len());
347    for (i, addr) in addresses.iter().enumerate() {
348        let _ = writeln!(output, "  [{}] address: {:?}", i, addr.address);
349        let tree_idx = Some(addr.address_merkle_tree_account_index);
350        let (tree_pubkey, _) = resolve_tree_and_queue_pubkeys(instruction_accounts, tree_idx, None);
351        if let Some(tp) = tree_pubkey {
352            let _ = writeln!(
353                output,
354                "      tree[{}]: {}",
355                addr.address_merkle_tree_account_index, tp
356            );
357        }
358    }
359}
360
361/// Format compress/decompress and relay fee section for Invoke/InvokeCpi.
362#[cfg(not(target_os = "solana"))]
363fn format_fee_section(
364    output: &mut String,
365    compress_or_decompress_lamports: Option<u64>,
366    is_compress: bool,
367    relay_fee: Option<u64>,
368) {
369    use std::fmt::Write;
370
371    if let Some(lamports) = compress_or_decompress_lamports {
372        let _ = writeln!(
373            output,
374            "Compress/Decompress: {} lamports (is_compress: {})",
375            lamports, is_compress
376        );
377    }
378
379    if let Some(fee) = relay_fee {
380        let _ = writeln!(output, "Relay fee: {} lamports", fee);
381    }
382}
383
384/// Format compress/decompress section for ReadOnly/AccountInfo variants.
385///
386/// Uses u64 directly instead of Option<u64>.
387#[cfg(not(target_os = "solana"))]
388fn format_compress_decompress_section(
389    output: &mut String,
390    compress_or_decompress_lamports: u64,
391    is_compress: bool,
392) {
393    use std::fmt::Write;
394
395    if compress_or_decompress_lamports > 0 {
396        let _ = writeln!(
397            output,
398            "Compress/Decompress: {} lamports (is_compress: {})",
399            compress_or_decompress_lamports, is_compress
400        );
401    }
402}
403
404/// Format account infos section for InvokeCpiWithAccountInfo.
405///
406/// Formats `CompressedAccountInfo` with combined input/output per account.
407#[cfg(not(target_os = "solana"))]
408fn format_account_infos_section(
409    output: &mut String,
410    account_infos: &[light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo],
411    instruction_accounts: &[AccountMeta],
412) {
413    use std::fmt::Write;
414
415    if account_infos.is_empty() {
416        return;
417    }
418
419    let _ = writeln!(output, "Account Infos ({}):", account_infos.len());
420    for (i, account_info) in account_infos.iter().enumerate() {
421        let _ = writeln!(output, "  [{}]", i);
422        if let Some(addr) = account_info.address {
423            let _ = writeln!(output, "      address: {:?}", addr);
424        }
425
426        if let Some(ref input) = account_info.input {
427            let _ = writeln!(output, "    Input:");
428            let _ = writeln!(output, "      lamports: {}", input.lamports);
429            let _ = writeln!(output, "      data_hash: {:?}", input.data_hash);
430            let _ = writeln!(output, "      discriminator: {:?}", input.discriminator);
431            let _ = writeln!(
432                output,
433                "      leaf_index: {}",
434                input.merkle_context.leaf_index
435            );
436            let _ = writeln!(output, "      root_index: {}", input.root_index);
437        }
438
439        if let Some(ref out) = account_info.output {
440            let _ = writeln!(output, "    Output:");
441            let _ = writeln!(output, "      lamports: {}", out.lamports);
442            let _ = writeln!(output, "      data_hash: {:?}", out.data_hash);
443            let _ = writeln!(output, "      discriminator: {:?}", out.discriminator);
444            if !out.data.is_empty() {
445                let _ = writeln!(
446                    output,
447                    "      data ({} bytes): {:?}",
448                    out.data.len(),
449                    out.data
450                );
451            }
452            let tree_idx = Some(out.output_merkle_tree_index);
453            let (tree_pubkey, _) =
454                resolve_tree_and_queue_pubkeys(instruction_accounts, tree_idx, None);
455            if let Some(tp) = tree_pubkey {
456                let _ = writeln!(
457                    output,
458                    "      merkle_tree_pubkey (index {}): {}",
459                    out.output_merkle_tree_index, tp
460                );
461            }
462        }
463    }
464}
465
466/// Helper to resolve merkle tree and queue pubkeys from instruction accounts.
467/// Tree accounts start 2 positions after the system program account.
468fn resolve_tree_and_queue_pubkeys(
469    accounts: &[AccountMeta],
470    merkle_tree_index: Option<u8>,
471    nullifier_queue_index: Option<u8>,
472) -> (Option<Pubkey>, Option<Pubkey>) {
473    let mut tree_pubkey = None;
474    let mut queue_pubkey = None;
475
476    // Find the system program account position
477    let mut system_program_pos = None;
478    for (i, account) in accounts.iter().enumerate() {
479        if account.pubkey.to_string() == SYSTEM_PROGRAM_ID {
480            system_program_pos = Some(i);
481            break;
482        }
483    }
484
485    if let Some(system_pos) = system_program_pos {
486        // Tree accounts start 2 positions after system program
487        let tree_accounts_start = system_pos + 2;
488
489        if let Some(tree_idx) = merkle_tree_index {
490            let tree_account_pos = tree_accounts_start + tree_idx as usize;
491            if tree_account_pos < accounts.len() {
492                tree_pubkey = Some(accounts[tree_account_pos].pubkey);
493            }
494        }
495
496        if let Some(queue_idx) = nullifier_queue_index {
497            let queue_account_pos = tree_accounts_start + queue_idx as usize;
498            if queue_account_pos < accounts.len() {
499                queue_pubkey = Some(accounts[queue_account_pos].pubkey);
500            }
501        }
502    }
503
504    (tree_pubkey, queue_pubkey)
505}
506
507/// Format InvokeCpiWithReadOnly instruction data.
508///
509/// Note: This instruction does NOT have the 4-byte Anchor prefix - it uses pure borsh.
510#[cfg(not(target_os = "solana"))]
511pub fn format_invoke_cpi_readonly(
512    data: &InstructionDataInvokeCpiWithReadOnly,
513    accounts: &[AccountMeta],
514) -> String {
515    use std::fmt::Write;
516    let mut output = String::new();
517
518    let _ = writeln!(
519        output,
520        "Accounts: in: {}, out: {}",
521        data.input_compressed_accounts.len(),
522        data.output_compressed_accounts.len()
523    );
524    let _ = writeln!(output, "Proof: Validity proof");
525
526    format_readonly_input_accounts_section(
527        &mut output,
528        &data.input_compressed_accounts,
529        &data.invoking_program_id,
530        accounts,
531    );
532    format_readonly_output_accounts_section(
533        &mut output,
534        &data.output_compressed_accounts,
535        &data.invoking_program_id,
536        accounts,
537    );
538    format_new_address_params_assigned_section(&mut output, &data.new_address_params, accounts);
539    format_read_only_addresses_section(&mut output, &data.read_only_addresses, accounts);
540    format_compress_decompress_section(
541        &mut output,
542        data.compress_or_decompress_lamports,
543        data.is_compress,
544    );
545
546    output
547}
548
549/// Resolve account names dynamically for InvokeCpiWithReadOnly.
550///
551/// Account layout depends on CPI context mode:
552///
553/// **CPI Context Write Mode** (`set_context || first_set_context`):
554/// - fee_payer, cpi_authority_pda, cpi_context
555///
556/// **Normal Mode**:
557/// 1. Fixed: fee_payer, authority, registered_program_pda, account_compression_authority,
558///    account_compression_program, system_program
559/// 2. Optional: cpi_context_account (if cpi_context is present)
560/// 3. Tree accounts: named based on usage in instruction data
561#[cfg(not(target_os = "solana"))]
562pub fn resolve_invoke_cpi_readonly_account_names(
563    data: &InstructionDataInvokeCpiWithReadOnly,
564    accounts: &[AccountMeta],
565) -> Vec<String> {
566    use std::collections::HashMap;
567
568    let mut names = Vec::with_capacity(accounts.len());
569    let mut idx = 0;
570    let mut known_pubkeys: HashMap<[u8; 32], String> = HashMap::new();
571
572    let mut add_name = |name: &str,
573                        accounts: &[AccountMeta],
574                        idx: &mut usize,
575                        known: &mut HashMap<[u8; 32], String>| {
576        if *idx < accounts.len() {
577            names.push(name.to_string());
578            known.insert(accounts[*idx].pubkey.to_bytes(), name.to_string());
579            *idx += 1;
580            true
581        } else {
582            false
583        }
584    };
585
586    // Check if we're in CPI context write mode
587    let cpi_context_write_mode = data.cpi_context.set_context || data.cpi_context.first_set_context;
588
589    if cpi_context_write_mode {
590        // CPI Context Write Mode: only 3 accounts
591        add_name("fee_payer", accounts, &mut idx, &mut known_pubkeys);
592        add_name("cpi_authority_pda", accounts, &mut idx, &mut known_pubkeys);
593        add_name("cpi_context", accounts, &mut idx, &mut known_pubkeys);
594        return names;
595    }
596
597    // Normal Mode: Fixed LightSystemAccounts (6 accounts)
598    add_name("fee_payer", accounts, &mut idx, &mut known_pubkeys);
599    add_name("authority", accounts, &mut idx, &mut known_pubkeys);
600    add_name(
601        "registered_program_pda",
602        accounts,
603        &mut idx,
604        &mut known_pubkeys,
605    );
606    add_name(
607        "account_compression_authority",
608        accounts,
609        &mut idx,
610        &mut known_pubkeys,
611    );
612    add_name(
613        "account_compression_program",
614        accounts,
615        &mut idx,
616        &mut known_pubkeys,
617    );
618    add_name("system_program", accounts, &mut idx, &mut known_pubkeys);
619
620    // Don't provide names for remaining accounts (cpi_context_account, tree/queue accounts)
621    // - let the formatter use the transaction-level account names
622    names
623}
624
625/// Format InvokeCpiWithAccountInfo instruction data.
626///
627/// Note: This instruction does NOT have the 4-byte Anchor prefix - it uses pure borsh.
628#[cfg(not(target_os = "solana"))]
629pub fn format_invoke_cpi_account_info(
630    data: &InstructionDataInvokeCpiWithAccountInfo,
631    accounts: &[AccountMeta],
632) -> String {
633    use std::fmt::Write;
634    let mut output = String::new();
635
636    let input_count = data
637        .account_infos
638        .iter()
639        .filter(|a| a.input.is_some())
640        .count();
641    let output_count = data
642        .account_infos
643        .iter()
644        .filter(|a| a.output.is_some())
645        .count();
646
647    let _ = writeln!(
648        output,
649        "Accounts: in: {}, out: {}",
650        input_count, output_count
651    );
652    let _ = writeln!(output, "Proof: Validity proof");
653
654    // Account infos with input/output (unique structure, kept inline)
655    format_account_infos_section(&mut output, &data.account_infos, accounts);
656
657    format_new_address_params_assigned_section(&mut output, &data.new_address_params, accounts);
658    format_read_only_addresses_section(&mut output, &data.read_only_addresses, accounts);
659    format_compress_decompress_section(
660        &mut output,
661        data.compress_or_decompress_lamports,
662        data.is_compress,
663    );
664
665    output
666}
667
668/// Resolve account names dynamically for InvokeCpiWithAccountInfo.
669///
670/// Account layout depends on CPI context mode:
671///
672/// **CPI Context Write Mode** (`set_context || first_set_context`):
673/// - fee_payer, cpi_authority_pda, cpi_context
674///
675/// **Normal Mode**:
676/// 1. Fixed: fee_payer, authority, registered_program_pda, account_compression_authority,
677///    account_compression_program, system_program
678/// 2. Optional: cpi_context_account (if cpi_context is present)
679/// 3. Tree accounts: named based on usage in instruction data
680#[cfg(not(target_os = "solana"))]
681pub fn resolve_invoke_cpi_account_info_account_names(
682    data: &InstructionDataInvokeCpiWithAccountInfo,
683    accounts: &[AccountMeta],
684) -> Vec<String> {
685    use std::collections::HashMap;
686
687    let mut names = Vec::with_capacity(accounts.len());
688    let mut idx = 0;
689    let mut known_pubkeys: HashMap<[u8; 32], String> = HashMap::new();
690
691    let mut add_name = |name: &str,
692                        accounts: &[AccountMeta],
693                        idx: &mut usize,
694                        known: &mut HashMap<[u8; 32], String>| {
695        if *idx < accounts.len() {
696            names.push(name.to_string());
697            known.insert(accounts[*idx].pubkey.to_bytes(), name.to_string());
698            *idx += 1;
699            true
700        } else {
701            false
702        }
703    };
704
705    // Check if we're in CPI context write mode
706    let cpi_context_write_mode = data.cpi_context.set_context || data.cpi_context.first_set_context;
707
708    if cpi_context_write_mode {
709        // CPI Context Write Mode: only 3 accounts
710        add_name("fee_payer", accounts, &mut idx, &mut known_pubkeys);
711        add_name("cpi_authority_pda", accounts, &mut idx, &mut known_pubkeys);
712        add_name("cpi_context", accounts, &mut idx, &mut known_pubkeys);
713        return names;
714    }
715
716    // Normal Mode: Fixed LightSystemAccounts (6 accounts)
717    add_name("fee_payer", accounts, &mut idx, &mut known_pubkeys);
718    add_name("authority", accounts, &mut idx, &mut known_pubkeys);
719    add_name(
720        "registered_program_pda",
721        accounts,
722        &mut idx,
723        &mut known_pubkeys,
724    );
725    add_name(
726        "account_compression_authority",
727        accounts,
728        &mut idx,
729        &mut known_pubkeys,
730    );
731    add_name(
732        "account_compression_program",
733        accounts,
734        &mut idx,
735        &mut known_pubkeys,
736    );
737    add_name("system_program", accounts, &mut idx, &mut known_pubkeys);
738
739    // Don't provide names for remaining accounts (cpi_context_account, tree/queue accounts)
740    // - let the formatter use the transaction-level account names
741    names
742}
743
744/// Wrapper type for Invoke instruction that handles the 4-byte Anchor prefix.
745///
746/// The derive macro's borsh deserialization expects the data immediately after
747/// the discriminator, but Invoke/InvokeCpi have a 4-byte vec length prefix.
748/// This wrapper type's deserialize implementation skips those 4 bytes.
749#[derive(Debug)]
750pub struct InvokeWrapper(pub InstructionDataInvoke);
751
752impl BorshDeserialize for InvokeWrapper {
753    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
754        // Skip 4-byte Anchor vec length prefix
755        let mut prefix = [0u8; 4];
756        reader.read_exact(&mut prefix)?;
757        // Deserialize the actual data
758        let inner = InstructionDataInvoke::deserialize_reader(reader)?;
759        Ok(InvokeWrapper(inner))
760    }
761}
762
763/// Wrapper type for InvokeCpi instruction that handles the 4-byte Anchor prefix.
764#[derive(Debug)]
765pub struct InvokeCpiWrapper(pub InstructionDataInvokeCpi);
766
767impl BorshDeserialize for InvokeCpiWrapper {
768    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
769        // Skip 4-byte Anchor vec length prefix
770        let mut prefix = [0u8; 4];
771        reader.read_exact(&mut prefix)?;
772        // Deserialize the actual data
773        let inner = InstructionDataInvokeCpi::deserialize_reader(reader)?;
774        Ok(InvokeCpiWrapper(inner))
775    }
776}
777
778/// Formatter wrapper that takes raw bytes and handles the prefix skip internally.
779#[cfg(not(target_os = "solana"))]
780pub fn format_invoke_wrapper(data: &InvokeWrapper, accounts: &[AccountMeta]) -> String {
781    // We already have the parsed data, format it directly
782    format_invoke_inner(&data.0, accounts)
783}
784
785/// Formatter wrapper that takes raw bytes and handles the prefix skip internally.
786#[cfg(not(target_os = "solana"))]
787pub fn format_invoke_cpi_wrapper(data: &InvokeCpiWrapper, accounts: &[AccountMeta]) -> String {
788    // We already have the parsed data, format it directly
789    format_invoke_cpi_inner(&data.0, accounts)
790}
791
792/// Format InstructionDataInvoke (internal helper).
793#[cfg(not(target_os = "solana"))]
794fn format_invoke_inner(data: &InstructionDataInvoke, accounts: &[AccountMeta]) -> String {
795    use std::fmt::Write;
796    let mut output = String::new();
797
798    let _ = writeln!(
799        output,
800        "Accounts: in: {}, out: {}",
801        data.input_compressed_accounts_with_merkle_context.len(),
802        data.output_compressed_accounts.len()
803    );
804
805    if data.proof.is_some() {
806        let _ = writeln!(output, "Proof: Validity proof");
807    }
808
809    format_input_accounts_section(
810        &mut output,
811        &data.input_compressed_accounts_with_merkle_context,
812        accounts,
813    );
814    format_output_accounts_section(&mut output, &data.output_compressed_accounts, accounts);
815    format_new_address_params_section(&mut output, &data.new_address_params, accounts);
816    format_fee_section(
817        &mut output,
818        data.compress_or_decompress_lamports,
819        data.is_compress,
820        data.relay_fee,
821    );
822
823    output
824}
825
826/// Format InstructionDataInvokeCpi (internal helper).
827#[cfg(not(target_os = "solana"))]
828fn format_invoke_cpi_inner(data: &InstructionDataInvokeCpi, accounts: &[AccountMeta]) -> String {
829    use std::fmt::Write;
830    let mut output = String::new();
831
832    let _ = writeln!(
833        output,
834        "Accounts: in: {}, out: {}",
835        data.input_compressed_accounts_with_merkle_context.len(),
836        data.output_compressed_accounts.len()
837    );
838
839    if data.proof.is_some() {
840        let _ = writeln!(output, "Proof: Validity proof");
841    }
842
843    format_input_accounts_section(
844        &mut output,
845        &data.input_compressed_accounts_with_merkle_context,
846        accounts,
847    );
848    format_output_accounts_section(&mut output, &data.output_compressed_accounts, accounts);
849    format_new_address_params_section(&mut output, &data.new_address_params, accounts);
850    format_fee_section(
851        &mut output,
852        data.compress_or_decompress_lamports,
853        data.is_compress,
854        data.relay_fee,
855    );
856
857    output
858}
859
860/// Light System Program instructions.
861///
862/// The Light System Program uses 8-byte discriminators for compressed account operations.
863/// Each instruction has an explicit discriminator attribute.
864#[derive(InstructionDecoder)]
865#[instruction_decoder(
866    program_id = "SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7",
867    program_name = "Light System Program",
868    discriminator_size = 8
869)]
870pub enum LightSystemInstruction {
871    /// Direct invocation of Light System - creates/modifies compressed accounts.
872    /// Has 4-byte Anchor vec length prefix after discriminator.
873    #[discriminator(26, 16, 169, 7, 21, 202, 242, 25)]
874    #[instruction_decoder(
875        account_names = ["fee_payer", "authority", "registered_program_pda", "log_program", "account_compression_authority", "account_compression_program", "self_program"],
876        params = InvokeWrapper,
877        pretty_formatter = crate::programs::light_system::format_invoke_wrapper
878    )]
879    Invoke,
880
881    /// CPI invocation from another program.
882    /// Has 4-byte Anchor vec length prefix after discriminator.
883    #[discriminator(49, 212, 191, 129, 39, 194, 43, 196)]
884    #[instruction_decoder(
885        account_names = ["fee_payer", "authority", "registered_program_pda", "log_program", "account_compression_authority", "account_compression_program", "invoking_program", "cpi_signer"],
886        params = InvokeCpiWrapper,
887        pretty_formatter = crate::programs::light_system::format_invoke_cpi_wrapper
888    )]
889    InvokeCpi,
890
891    /// CPI with read-only compressed accounts (V2 account layout).
892    /// Uses pure borsh serialization (no 4-byte prefix).
893    /// Note: V2 instructions have no log_program account.
894    #[discriminator(86, 47, 163, 166, 21, 223, 92, 8)]
895    #[instruction_decoder(
896        params = InstructionDataInvokeCpiWithReadOnly,
897        account_names_resolver_from_params = crate::programs::light_system::resolve_invoke_cpi_readonly_account_names,
898        pretty_formatter = crate::programs::light_system::format_invoke_cpi_readonly
899    )]
900    InvokeCpiWithReadOnly,
901
902    /// CPI with full account info for each compressed account (V2 account layout).
903    /// Uses pure borsh serialization (no 4-byte prefix).
904    /// Note: V2 instructions have no log_program account.
905    #[discriminator(228, 34, 128, 84, 47, 139, 86, 240)]
906    #[instruction_decoder(
907        params = InstructionDataInvokeCpiWithAccountInfo,
908        account_names_resolver_from_params = crate::programs::light_system::resolve_invoke_cpi_account_info_account_names,
909        pretty_formatter = crate::programs::light_system::format_invoke_cpi_account_info
910    )]
911    InvokeCpiWithAccountInfo,
912}