light_program_test/logging/
decoder.rs

1//! Instruction decoder for Light Protocol and common Solana programs
2
3use borsh::BorshDeserialize;
4use light_compressed_account::instruction_data::{
5    data::InstructionDataInvoke, invoke_cpi::InstructionDataInvokeCpi,
6    with_account_info::InstructionDataInvokeCpiWithAccountInfo,
7    with_readonly::InstructionDataInvokeCpiWithReadOnly,
8};
9use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, system_program};
10
11use super::types::ParsedInstructionData;
12
13/// Helper to resolve merkle tree and queue pubkeys from instruction accounts
14/// For InvokeCpi instructions, tree accounts start 2 positions after the system program
15fn resolve_tree_and_queue_pubkeys(
16    accounts: &[AccountMeta],
17    merkle_tree_index: Option<u8>,
18    nullifier_queue_index: Option<u8>,
19) -> (Option<Pubkey>, Option<Pubkey>) {
20    let mut tree_pubkey = None;
21    let mut queue_pubkey = None;
22
23    // Find the system program account position
24    let mut system_program_pos = None;
25    for (i, account) in accounts.iter().enumerate() {
26        if account.pubkey == system_program::ID {
27            system_program_pos = Some(i);
28            break;
29        }
30    }
31
32    if let Some(system_pos) = system_program_pos {
33        // Tree accounts start 2 positions after system program
34        let tree_accounts_start = system_pos + 2;
35
36        if let Some(tree_idx) = merkle_tree_index {
37            let tree_account_pos = tree_accounts_start + tree_idx as usize;
38            if tree_account_pos < accounts.len() {
39                tree_pubkey = Some(accounts[tree_account_pos].pubkey);
40            }
41        }
42
43        if let Some(queue_idx) = nullifier_queue_index {
44            let queue_account_pos = tree_accounts_start + queue_idx as usize;
45            if queue_account_pos < accounts.len() {
46                queue_pubkey = Some(accounts[queue_account_pos].pubkey);
47            }
48        }
49    }
50
51    (tree_pubkey, queue_pubkey)
52}
53
54/// Decode instruction data for known programs
55pub fn decode_instruction(
56    program_id: &Pubkey,
57    data: &[u8],
58    accounts: &[AccountMeta],
59) -> Option<ParsedInstructionData> {
60    match program_id.to_string().as_str() {
61        // Light System Program
62        "SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7" => {
63            decode_light_system_instruction(data, accounts, program_id)
64        }
65
66        // Compute Budget Program
67        "ComputeBudget111111111111111111111111111111" => decode_compute_budget_instruction(data),
68
69        // System Program
70        id if id == system_program::ID.to_string() => decode_system_instruction(data),
71
72        // Account Compression Program
73        "compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq" => decode_compression_instruction(data),
74
75        // Compressed Token Program
76        "cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m" => decode_compressed_token_instruction(data),
77
78        _ => Some(ParsedInstructionData::Unknown {
79            program_name: get_program_name(program_id),
80            data_preview: bs58::encode(&data[..data.len().min(16)]).into_string(),
81        }),
82    }
83}
84
85/// Decode Light System Program instructions
86fn decode_light_system_instruction(
87    data: &[u8],
88    accounts: &[AccountMeta],
89    program_id: &Pubkey,
90) -> Option<ParsedInstructionData> {
91    if data.is_empty() {
92        return None;
93    }
94
95    // Light System Program uses 8-byte discriminators
96    if data.len() < 8 {
97        return Some(ParsedInstructionData::LightSystemProgram {
98            instruction_type: "Invalid".to_string(),
99            compressed_accounts: None,
100            proof_info: None,
101            address_params: None,
102            fee_info: None,
103            input_account_data: None,
104            output_account_data: None,
105        });
106    }
107
108    // Extract the 8-byte discriminator
109    let discriminator: [u8; 8] = data[0..8].try_into().unwrap();
110
111    // Light Protocol discriminators from compressed-account/src/discriminators.rs
112    let (
113        instruction_type,
114        compressed_accounts,
115        proof_info,
116        address_params,
117        fee_info,
118        input_account_data,
119        output_account_data,
120    ) = match discriminator {
121        [26, 16, 169, 7, 21, 202, 242, 25] => {
122            // DISCRIMINATOR_INVOKE
123            match parse_invoke_instruction(&data[8..], accounts) {
124                Ok(parsed) => parsed,
125                Err(_) => (
126                    "Invoke (parse error)".to_string(),
127                    None,
128                    None,
129                    None,
130                    None,
131                    None,
132                    None,
133                ),
134            }
135        }
136        [49, 212, 191, 129, 39, 194, 43, 196] => {
137            // DISCRIMINATOR_INVOKE_CPI
138            match parse_invoke_cpi_instruction(&data[8..], accounts) {
139                Ok(parsed) => parsed,
140                Err(_) => (
141                    "InvokeCpi (parse error)".to_string(),
142                    None,
143                    None,
144                    None,
145                    None,
146                    None,
147                    None,
148                ),
149            }
150        }
151        [86, 47, 163, 166, 21, 223, 92, 8] => {
152            // DISCRIMINATOR_INVOKE_CPI_WITH_READ_ONLY
153            match parse_invoke_cpi_readonly_instruction(&data[8..], accounts) {
154                Ok(parsed) => parsed,
155                Err(_) => (
156                    "InvokeCpiWithReadOnly (parse error)".to_string(),
157                    None,
158                    None,
159                    None,
160                    None,
161                    None,
162                    None,
163                ),
164            }
165        }
166        [228, 34, 128, 84, 47, 139, 86, 240] => {
167            // INVOKE_CPI_WITH_ACCOUNT_INFO_INSTRUCTION
168            match parse_invoke_cpi_account_info_instruction(&data[8..], accounts, program_id) {
169                Ok(parsed) => parsed,
170                Err(_) => (
171                    "InvokeCpiWithAccountInfo (parse error)".to_string(),
172                    None,
173                    None,
174                    None,
175                    None,
176                    None,
177                    None,
178                ),
179            }
180        }
181        _ => {
182            // Unknown discriminator - show the discriminator bytes for debugging
183            let discriminator_str = format!("{:?}", discriminator);
184            (
185                format!("Unknown({})", discriminator_str),
186                None,
187                None,
188                None,
189                None,
190                None,
191                None,
192            )
193        }
194    };
195
196    Some(ParsedInstructionData::LightSystemProgram {
197        instruction_type,
198        compressed_accounts,
199        proof_info,
200        address_params,
201        fee_info,
202        input_account_data,
203        output_account_data,
204    })
205}
206
207type InstructionParseResult = Result<
208    (
209        String,
210        Option<super::types::CompressedAccountSummary>,
211        Option<super::types::ProofSummary>,
212        Option<Vec<super::types::AddressParam>>,
213        Option<super::types::FeeSummary>,
214        Option<Vec<super::types::InputAccountData>>,
215        Option<Vec<super::types::OutputAccountData>>,
216    ),
217    Box<dyn std::error::Error>,
218>;
219
220/// Parse Invoke instruction data - display data hashes directly
221fn parse_invoke_instruction(data: &[u8], accounts: &[AccountMeta]) -> InstructionParseResult {
222    // Skip the 4-byte vec length prefix that Anchor adds
223    if data.len() < 4 {
224        return Err("Instruction data too short for Anchor prefix".into());
225    }
226    let instruction_data = InstructionDataInvoke::try_from_slice(&data[4..])?;
227
228    let compressed_accounts = Some(super::types::CompressedAccountSummary {
229        input_accounts: instruction_data
230            .input_compressed_accounts_with_merkle_context
231            .len(),
232        output_accounts: instruction_data.output_compressed_accounts.len(),
233        lamports_change: instruction_data
234            .compress_or_decompress_lamports
235            .map(|l| l as i64),
236    });
237
238    let proof_info = instruction_data
239        .proof
240        .as_ref()
241        .map(|_| super::types::ProofSummary {
242            proof_type: "Validity".to_string(),
243            has_validity_proof: true,
244        });
245
246    // Extract actual address parameters with values
247    let address_params = if !instruction_data.new_address_params.is_empty() {
248        Some(
249            instruction_data
250                .new_address_params
251                .iter()
252                .map(|param| {
253                    let tree_idx = Some(param.address_merkle_tree_account_index);
254                    let queue_idx = Some(param.address_queue_account_index);
255                    let (tree_pubkey, queue_pubkey) =
256                        resolve_tree_and_queue_pubkeys(accounts, tree_idx, queue_idx);
257
258                    super::types::AddressParam {
259                        seed: param.seed,
260                        address_queue_index: queue_idx,
261                        address_queue_pubkey: queue_pubkey,
262                        merkle_tree_index: tree_idx,
263                        address_merkle_tree_pubkey: tree_pubkey,
264                        root_index: Some(param.address_merkle_tree_root_index),
265                        derived_address: None,
266                        assigned_account_index: super::types::AddressAssignment::V1,
267                    }
268                })
269                .collect(),
270        )
271    } else {
272        None
273    };
274
275    // Extract input account data
276    let input_account_data = if !instruction_data
277        .input_compressed_accounts_with_merkle_context
278        .is_empty()
279    {
280        Some(
281            instruction_data
282                .input_compressed_accounts_with_merkle_context
283                .iter()
284                .map(|acc| {
285                    let tree_idx = Some(acc.merkle_context.merkle_tree_pubkey_index);
286                    let queue_idx = Some(acc.merkle_context.queue_pubkey_index);
287                    let (tree_pubkey, queue_pubkey) =
288                        resolve_tree_and_queue_pubkeys(accounts, tree_idx, queue_idx);
289
290                    super::types::InputAccountData {
291                        lamports: acc.compressed_account.lamports,
292                        owner: Some(acc.compressed_account.owner.into()),
293                        merkle_tree_index: tree_idx,
294                        merkle_tree_pubkey: tree_pubkey,
295                        queue_index: queue_idx,
296                        queue_pubkey,
297                        address: acc.compressed_account.address,
298                        data_hash: if let Some(ref data) = acc.compressed_account.data {
299                            data.data_hash.to_vec()
300                        } else {
301                            vec![]
302                        },
303                        discriminator: if let Some(ref data) = acc.compressed_account.data {
304                            data.discriminator.to_vec()
305                        } else {
306                            vec![]
307                        },
308                        leaf_index: Some(acc.merkle_context.leaf_index),
309                        root_index: Some(acc.root_index),
310                    }
311                })
312                .collect(),
313        )
314    } else {
315        None
316    };
317
318    // Extract output account data
319    let output_account_data = if !instruction_data.output_compressed_accounts.is_empty() {
320        Some(
321            instruction_data
322                .output_compressed_accounts
323                .iter()
324                .map(|acc| {
325                    let tree_idx = Some(acc.merkle_tree_index);
326                    let (tree_pubkey, _queue_pubkey) =
327                        resolve_tree_and_queue_pubkeys(accounts, tree_idx, None);
328
329                    super::types::OutputAccountData {
330                        lamports: acc.compressed_account.lamports,
331                        data: acc.compressed_account.data.as_ref().map(|d| d.data.clone()),
332                        owner: Some(acc.compressed_account.owner.into()),
333                        merkle_tree_index: tree_idx,
334                        merkle_tree_pubkey: tree_pubkey,
335                        queue_index: None,
336                        queue_pubkey: None,
337                        address: acc.compressed_account.address,
338                        data_hash: if let Some(ref data) = acc.compressed_account.data {
339                            data.data_hash.to_vec()
340                        } else {
341                            vec![]
342                        },
343                        discriminator: if let Some(ref data) = acc.compressed_account.data {
344                            data.discriminator.to_vec()
345                        } else {
346                            vec![]
347                        },
348                    }
349                })
350                .collect(),
351        )
352    } else {
353        None
354    };
355
356    let fee_info = instruction_data
357        .relay_fee
358        .map(|fee| super::types::FeeSummary {
359            relay_fee: Some(fee),
360            compression_fee: None,
361        });
362
363    Ok((
364        "Invoke".to_string(),
365        compressed_accounts,
366        proof_info,
367        address_params,
368        fee_info,
369        input_account_data,
370        output_account_data,
371    ))
372}
373
374/// Parse InvokeCpi instruction data - display data hashes directly
375fn parse_invoke_cpi_instruction(data: &[u8], accounts: &[AccountMeta]) -> InstructionParseResult {
376    // Skip the 4-byte vec length prefix that Anchor adds
377    if data.len() < 4 {
378        return Err("Instruction data too short for Anchor prefix".into());
379    }
380    let instruction_data = InstructionDataInvokeCpi::try_from_slice(&data[4..])?;
381
382    let compressed_accounts = Some(super::types::CompressedAccountSummary {
383        input_accounts: instruction_data
384            .input_compressed_accounts_with_merkle_context
385            .len(),
386        output_accounts: instruction_data.output_compressed_accounts.len(),
387        lamports_change: instruction_data
388            .compress_or_decompress_lamports
389            .map(|l| l as i64),
390    });
391
392    let proof_info = instruction_data
393        .proof
394        .as_ref()
395        .map(|_| super::types::ProofSummary {
396            proof_type: "Validity".to_string(),
397            has_validity_proof: true,
398        });
399
400    // Extract actual address parameters with values
401    let address_params = if !instruction_data.new_address_params.is_empty() {
402        Some(
403            instruction_data
404                .new_address_params
405                .iter()
406                .map(|param| {
407                    let tree_idx = Some(param.address_merkle_tree_account_index);
408                    let queue_idx = Some(param.address_queue_account_index);
409                    let (tree_pubkey, queue_pubkey) =
410                        resolve_tree_and_queue_pubkeys(accounts, tree_idx, queue_idx);
411
412                    super::types::AddressParam {
413                        seed: param.seed,
414                        address_queue_index: queue_idx,
415                        address_queue_pubkey: queue_pubkey,
416                        merkle_tree_index: tree_idx,
417                        address_merkle_tree_pubkey: tree_pubkey,
418                        root_index: Some(param.address_merkle_tree_root_index),
419                        derived_address: None,
420                        assigned_account_index: super::types::AddressAssignment::V1,
421                    }
422                })
423                .collect(),
424        )
425    } else {
426        None
427    };
428
429    // Extract input account data
430    let input_account_data = if !instruction_data
431        .input_compressed_accounts_with_merkle_context
432        .is_empty()
433    {
434        Some(
435            instruction_data
436                .input_compressed_accounts_with_merkle_context
437                .iter()
438                .map(|acc| {
439                    let tree_idx = Some(acc.merkle_context.merkle_tree_pubkey_index);
440                    let queue_idx = Some(acc.merkle_context.queue_pubkey_index);
441                    let (tree_pubkey, queue_pubkey) =
442                        resolve_tree_and_queue_pubkeys(accounts, tree_idx, queue_idx);
443
444                    super::types::InputAccountData {
445                        lamports: acc.compressed_account.lamports,
446                        owner: Some(acc.compressed_account.owner.into()),
447                        merkle_tree_index: tree_idx,
448                        merkle_tree_pubkey: tree_pubkey,
449                        queue_index: queue_idx,
450                        queue_pubkey,
451                        address: acc.compressed_account.address,
452                        data_hash: if let Some(ref data) = acc.compressed_account.data {
453                            data.data_hash.to_vec()
454                        } else {
455                            vec![]
456                        },
457                        discriminator: if let Some(ref data) = acc.compressed_account.data {
458                            data.discriminator.to_vec()
459                        } else {
460                            vec![]
461                        },
462                        leaf_index: Some(acc.merkle_context.leaf_index),
463                        root_index: Some(acc.root_index),
464                    }
465                })
466                .collect(),
467        )
468    } else {
469        None
470    };
471
472    // Extract output account data
473    let output_account_data = if !instruction_data.output_compressed_accounts.is_empty() {
474        Some(
475            instruction_data
476                .output_compressed_accounts
477                .iter()
478                .map(|acc| {
479                    let tree_idx = Some(acc.merkle_tree_index);
480                    let (tree_pubkey, _queue_pubkey) =
481                        resolve_tree_and_queue_pubkeys(accounts, tree_idx, None);
482
483                    super::types::OutputAccountData {
484                        lamports: acc.compressed_account.lamports,
485                        data: acc.compressed_account.data.as_ref().map(|d| d.data.clone()),
486                        owner: Some(acc.compressed_account.owner.into()),
487                        merkle_tree_index: tree_idx,
488                        merkle_tree_pubkey: tree_pubkey,
489                        queue_index: None,
490                        queue_pubkey: None,
491                        address: acc.compressed_account.address,
492                        data_hash: if let Some(ref data) = acc.compressed_account.data {
493                            data.data_hash.to_vec()
494                        } else {
495                            vec![]
496                        },
497                        discriminator: if let Some(ref data) = acc.compressed_account.data {
498                            data.discriminator.to_vec()
499                        } else {
500                            vec![]
501                        },
502                    }
503                })
504                .collect(),
505        )
506    } else {
507        None
508    };
509
510    let fee_info = instruction_data
511        .relay_fee
512        .map(|fee| super::types::FeeSummary {
513            relay_fee: Some(fee),
514            compression_fee: None,
515        });
516
517    Ok((
518        "InvokeCpi".to_string(),
519        compressed_accounts,
520        proof_info,
521        address_params,
522        fee_info,
523        input_account_data,
524        output_account_data,
525    ))
526}
527
528/// Parse InvokeCpiWithReadOnly instruction data - display data hashes directly
529fn parse_invoke_cpi_readonly_instruction(
530    data: &[u8],
531    accounts: &[AccountMeta],
532) -> InstructionParseResult {
533    let instruction_data = InstructionDataInvokeCpiWithReadOnly::try_from_slice(data)?;
534
535    let compressed_accounts = Some(super::types::CompressedAccountSummary {
536        input_accounts: instruction_data.input_compressed_accounts.len(),
537        output_accounts: instruction_data.output_compressed_accounts.len(),
538        lamports_change: if instruction_data.compress_or_decompress_lamports > 0 {
539            Some(instruction_data.compress_or_decompress_lamports as i64)
540        } else {
541            None
542        },
543    });
544
545    let proof_info = Some(super::types::ProofSummary {
546        proof_type: "Validity".to_string(),
547        has_validity_proof: true,
548    });
549
550    // Extract actual address parameters with values
551    let mut address_params = Vec::new();
552
553    // Add new address parameters with actual values
554    for param in &instruction_data.new_address_params {
555        let tree_idx = Some(param.address_merkle_tree_account_index);
556        let queue_idx = Some(param.address_queue_account_index);
557        let (tree_pubkey, queue_pubkey) =
558            resolve_tree_and_queue_pubkeys(accounts, tree_idx, queue_idx);
559
560        address_params.push(super::types::AddressParam {
561            seed: param.seed,
562            address_queue_index: queue_idx,
563            address_queue_pubkey: queue_pubkey,
564            merkle_tree_index: tree_idx,
565            address_merkle_tree_pubkey: tree_pubkey,
566            root_index: Some(param.address_merkle_tree_root_index),
567            derived_address: None,
568            assigned_account_index: if param.assigned_to_account {
569                super::types::AddressAssignment::AssignedIndex(param.assigned_account_index)
570            } else {
571                super::types::AddressAssignment::None
572            },
573        });
574    }
575
576    // Add readonly address parameters
577    for readonly_addr in &instruction_data.read_only_addresses {
578        let tree_idx = Some(readonly_addr.address_merkle_tree_account_index);
579        let (tree_pubkey, _queue_pubkey) = resolve_tree_and_queue_pubkeys(accounts, tree_idx, None);
580
581        address_params.push(super::types::AddressParam {
582            seed: [0; 32], // ReadOnly addresses don't have seeds in the same way
583            address_queue_index: None,
584            address_queue_pubkey: None,
585            merkle_tree_index: tree_idx,
586            address_merkle_tree_pubkey: tree_pubkey,
587            root_index: Some(readonly_addr.address_merkle_tree_root_index),
588            derived_address: Some(readonly_addr.address),
589            assigned_account_index: super::types::AddressAssignment::None,
590        });
591    }
592
593    let address_params = if !address_params.is_empty() {
594        Some(address_params)
595    } else {
596        None
597    };
598
599    // Extract input account data - use data_hash from InAccount
600    let input_account_data = if !instruction_data.input_compressed_accounts.is_empty() {
601        Some(
602            instruction_data
603                .input_compressed_accounts
604                .iter()
605                .map(|acc| {
606                    let tree_idx = Some(acc.merkle_context.merkle_tree_pubkey_index);
607                    let queue_idx = Some(acc.merkle_context.queue_pubkey_index);
608                    let (tree_pubkey, queue_pubkey) =
609                        resolve_tree_and_queue_pubkeys(accounts, tree_idx, queue_idx);
610
611                    super::types::InputAccountData {
612                        lamports: acc.lamports,
613                        owner: Some(instruction_data.invoking_program_id.into()), // Use invoking program as owner
614                        merkle_tree_index: tree_idx,
615                        merkle_tree_pubkey: tree_pubkey,
616                        queue_index: queue_idx,
617                        queue_pubkey,
618                        address: acc.address,
619                        data_hash: acc.data_hash.to_vec(),
620                        discriminator: acc.discriminator.to_vec(),
621                        leaf_index: Some(acc.merkle_context.leaf_index),
622                        root_index: Some(acc.root_index),
623                    }
624                })
625                .collect(),
626        )
627    } else {
628        None
629    };
630
631    // Extract output account data
632    let output_account_data = if !instruction_data.output_compressed_accounts.is_empty() {
633        Some(
634            instruction_data
635                .output_compressed_accounts
636                .iter()
637                .map(|acc| {
638                    let tree_idx = Some(acc.merkle_tree_index);
639                    let (tree_pubkey, _queue_pubkey) =
640                        resolve_tree_and_queue_pubkeys(accounts, tree_idx, None);
641
642                    super::types::OutputAccountData {
643                        lamports: acc.compressed_account.lamports,
644                        data: acc.compressed_account.data.as_ref().map(|d| d.data.clone()),
645                        owner: Some(instruction_data.invoking_program_id.into()), // Use invoking program as owner for consistency
646                        merkle_tree_index: tree_idx,
647                        merkle_tree_pubkey: tree_pubkey,
648                        queue_index: None,
649                        queue_pubkey: None,
650                        address: acc.compressed_account.address,
651                        data_hash: if let Some(ref data) = acc.compressed_account.data {
652                            data.data_hash.to_vec()
653                        } else {
654                            vec![]
655                        },
656                        discriminator: if let Some(ref data) = acc.compressed_account.data {
657                            data.discriminator.to_vec()
658                        } else {
659                            vec![]
660                        },
661                    }
662                })
663                .collect(),
664        )
665    } else {
666        None
667    };
668
669    Ok((
670        "InvokeCpiWithReadOnly".to_string(),
671        compressed_accounts,
672        proof_info,
673        address_params,
674        None,
675        input_account_data,
676        output_account_data,
677    ))
678}
679
680/// Parse InvokeCpiWithAccountInfo instruction data - display data hashes directly
681fn parse_invoke_cpi_account_info_instruction(
682    data: &[u8],
683    accounts: &[AccountMeta],
684    program_id: &Pubkey,
685) -> InstructionParseResult {
686    let instruction_data = InstructionDataInvokeCpiWithAccountInfo::try_from_slice(data)?;
687
688    let input_accounts = instruction_data
689        .account_infos
690        .iter()
691        .filter(|a| a.input.is_some())
692        .count();
693    let output_accounts = instruction_data
694        .account_infos
695        .iter()
696        .filter(|a| a.output.is_some())
697        .count();
698
699    let compressed_accounts = Some(super::types::CompressedAccountSummary {
700        input_accounts,
701        output_accounts,
702        lamports_change: if instruction_data.compress_or_decompress_lamports > 0 {
703            Some(instruction_data.compress_or_decompress_lamports as i64)
704        } else {
705            None
706        },
707    });
708
709    let proof_info = Some(super::types::ProofSummary {
710        proof_type: "Validity".to_string(),
711        has_validity_proof: true,
712    });
713
714    // Extract actual address parameters with values
715    let mut address_params = Vec::new();
716
717    // Add new address parameters with actual values
718    for param in &instruction_data.new_address_params {
719        let tree_idx = Some(param.address_merkle_tree_account_index);
720        let queue_idx = Some(param.address_queue_account_index);
721        let (tree_pubkey, queue_pubkey) =
722            resolve_tree_and_queue_pubkeys(accounts, tree_idx, queue_idx);
723
724        address_params.push(super::types::AddressParam {
725            seed: param.seed,
726            address_queue_index: queue_idx,
727            address_queue_pubkey: queue_pubkey,
728            merkle_tree_index: tree_idx,
729            address_merkle_tree_pubkey: tree_pubkey,
730            root_index: Some(param.address_merkle_tree_root_index),
731            derived_address: None,
732            assigned_account_index: if param.assigned_to_account {
733                super::types::AddressAssignment::AssignedIndex(param.assigned_account_index)
734            } else {
735                super::types::AddressAssignment::None
736            },
737        });
738    }
739
740    // Add readonly address parameters
741    for readonly_addr in &instruction_data.read_only_addresses {
742        let tree_idx = Some(readonly_addr.address_merkle_tree_account_index);
743        let (tree_pubkey, _queue_pubkey) = resolve_tree_and_queue_pubkeys(accounts, tree_idx, None);
744
745        address_params.push(super::types::AddressParam {
746            seed: [0; 32], // ReadOnly addresses don't have seeds in the same way
747            address_queue_index: None,
748            address_queue_pubkey: None,
749            merkle_tree_index: tree_idx,
750            address_merkle_tree_pubkey: tree_pubkey,
751            root_index: Some(readonly_addr.address_merkle_tree_root_index),
752            derived_address: Some(readonly_addr.address),
753            assigned_account_index: super::types::AddressAssignment::None,
754        });
755    }
756
757    let address_params = if !address_params.is_empty() {
758        Some(address_params)
759    } else {
760        None
761    };
762
763    // Extract input account data from account_infos
764    let input_account_data = {
765        let mut input_data = Vec::new();
766        for account_info in &instruction_data.account_infos {
767            if let Some(ref input) = account_info.input {
768                input_data.push(super::types::InputAccountData {
769                    lamports: input.lamports,
770                    owner: Some(*program_id), // Use invoking program as owner
771                    merkle_tree_index: None, // Note: merkle tree context not available in CompressedAccountInfo
772                    merkle_tree_pubkey: None,
773                    queue_index: None,
774                    queue_pubkey: None,
775                    address: account_info.address, // Use address from CompressedAccountInfo
776                    data_hash: input.data_hash.to_vec(),
777                    discriminator: input.discriminator.to_vec(),
778                    leaf_index: Some(input.merkle_context.leaf_index),
779                    root_index: Some(input.root_index),
780                });
781            }
782        }
783        if !input_data.is_empty() {
784            Some(input_data)
785        } else {
786            None
787        }
788    };
789
790    // Extract output account data from account_infos
791    let output_account_data = {
792        let mut output_data = Vec::new();
793        for account_info in &instruction_data.account_infos {
794            if let Some(ref output) = account_info.output {
795                let tree_idx = Some(output.output_merkle_tree_index);
796                let (tree_pubkey, _queue_pubkey) =
797                    resolve_tree_and_queue_pubkeys(accounts, tree_idx, None);
798
799                output_data.push(super::types::OutputAccountData {
800                    lamports: output.lamports,
801                    data: if !output.data.is_empty() {
802                        Some(output.data.clone())
803                    } else {
804                        None
805                    },
806                    owner: Some(*program_id), // Use invoking program as owner
807                    merkle_tree_index: tree_idx,
808                    merkle_tree_pubkey: tree_pubkey,
809                    queue_index: None,
810                    queue_pubkey: None,
811                    address: account_info.address, // Use address from CompressedAccountInfo
812                    data_hash: output.data_hash.to_vec(),
813                    discriminator: output.discriminator.to_vec(),
814                });
815            }
816        }
817        if !output_data.is_empty() {
818            Some(output_data)
819        } else {
820            None
821        }
822    };
823
824    Ok((
825        "InvokeCpiWithAccountInfo".to_string(),
826        compressed_accounts,
827        proof_info,
828        address_params,
829        None,
830        input_account_data,
831        output_account_data,
832    ))
833}
834
835/// Decode Compute Budget Program instructions
836fn decode_compute_budget_instruction(data: &[u8]) -> Option<ParsedInstructionData> {
837    if data.len() < 4 {
838        return None;
839    }
840
841    let instruction_discriminator = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
842
843    match instruction_discriminator {
844        0 => {
845            // RequestUnitsDeprecated
846            if data.len() >= 12 {
847                let units = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as u64;
848                let _additional_fee =
849                    u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as u64;
850                Some(ParsedInstructionData::ComputeBudget {
851                    instruction_type: "RequestUnitsDeprecated".to_string(),
852                    value: Some(units),
853                })
854            } else {
855                None
856            }
857        }
858        1 => {
859            // RequestHeapFrame
860            if data.len() >= 8 {
861                let bytes = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as u64;
862                Some(ParsedInstructionData::ComputeBudget {
863                    instruction_type: "RequestHeapFrame".to_string(),
864                    value: Some(bytes),
865                })
866            } else {
867                None
868            }
869        }
870        2 => {
871            // SetComputeUnitLimit
872            if data.len() >= 8 {
873                let units = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as u64;
874                Some(ParsedInstructionData::ComputeBudget {
875                    instruction_type: "SetComputeUnitLimit".to_string(),
876                    value: Some(units),
877                })
878            } else {
879                None
880            }
881        }
882        3 => {
883            // SetComputeUnitPrice
884            if data.len() >= 12 {
885                let price = u64::from_le_bytes([
886                    data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
887                ]);
888                Some(ParsedInstructionData::ComputeBudget {
889                    instruction_type: "SetComputeUnitPrice".to_string(),
890                    value: Some(price),
891                })
892            } else {
893                None
894            }
895        }
896        _ => Some(ParsedInstructionData::ComputeBudget {
897            instruction_type: "Unknown".to_string(),
898            value: None,
899        }),
900    }
901}
902
903/// Decode System Program instructions
904fn decode_system_instruction(data: &[u8]) -> Option<ParsedInstructionData> {
905    if data.len() < 4 {
906        return None;
907    }
908
909    let instruction_type = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
910
911    match instruction_type {
912        0 => {
913            // CreateAccount
914            if data.len() >= 52 {
915                let lamports = u64::from_le_bytes([
916                    data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
917                ]);
918                let space = u64::from_le_bytes([
919                    data[12], data[13], data[14], data[15], data[16], data[17], data[18], data[19],
920                ]);
921
922                Some(ParsedInstructionData::System {
923                    instruction_type: "CreateAccount".to_string(),
924                    lamports: Some(lamports),
925                    space: Some(space),
926                    new_account: None,
927                })
928            } else {
929                None
930            }
931        }
932        2 => {
933            // Transfer
934            if data.len() >= 12 {
935                let lamports = u64::from_le_bytes([
936                    data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
937                ]);
938
939                Some(ParsedInstructionData::System {
940                    instruction_type: "Transfer".to_string(),
941                    lamports: Some(lamports),
942                    space: None,
943                    new_account: None,
944                })
945            } else {
946                None
947            }
948        }
949        8 => {
950            // Allocate
951            if data.len() >= 12 {
952                let space = u64::from_le_bytes([
953                    data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
954                ]);
955
956                Some(ParsedInstructionData::System {
957                    instruction_type: "Allocate".to_string(),
958                    lamports: None,
959                    space: Some(space),
960                    new_account: None,
961                })
962            } else {
963                None
964            }
965        }
966        _ => Some(ParsedInstructionData::System {
967            instruction_type: "Unknown".to_string(),
968            lamports: None,
969            space: None,
970            new_account: None,
971        }),
972    }
973}
974
975/// Decode Account Compression Program instructions
976fn decode_compression_instruction(data: &[u8]) -> Option<ParsedInstructionData> {
977    // Return basic instruction info for account compression
978    let instruction_name = if data.len() >= 8 {
979        // Common account compression operations
980        "InsertIntoQueues"
981    } else {
982        "Unknown"
983    };
984
985    Some(ParsedInstructionData::Unknown {
986        program_name: "Account Compression".to_string(),
987        data_preview: format!("{}({}bytes)", instruction_name, data.len()),
988    })
989}
990
991/// Decode Compressed Token Program instructions
992fn decode_compressed_token_instruction(data: &[u8]) -> Option<ParsedInstructionData> {
993    // Return basic instruction info for compressed token operations
994    let instruction_name = if data.len() >= 8 {
995        // Common compressed token operations
996        "TokenOperation"
997    } else {
998        "Unknown"
999    };
1000
1001    Some(ParsedInstructionData::Unknown {
1002        program_name: "Compressed Token".to_string(),
1003        data_preview: format!("{}({}bytes)", instruction_name, data.len()),
1004    })
1005}
1006
1007/// Get human-readable program name
1008fn get_program_name(program_id: &Pubkey) -> String {
1009    match program_id.to_string().as_str() {
1010        id if id == system_program::ID.to_string() => "System Program".to_string(),
1011        "ComputeBudget111111111111111111111111111111" => "Compute Budget".to_string(),
1012        "SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7" => "Light System Program".to_string(),
1013        "compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq" => "Account Compression".to_string(),
1014        "FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy" => "Test Program".to_string(),
1015        _ => {
1016            let pubkey_str = program_id.to_string();
1017            format!("Program {}", &pubkey_str[..8])
1018        }
1019    }
1020}
1021
1022/// Extract Light Protocol events from transaction logs and metadata
1023pub fn extract_light_events(
1024    logs: &[String],
1025    _events: &Option<Vec<String>>, // Light Protocol events for future enhancement
1026) -> Vec<super::types::LightProtocolEvent> {
1027    let mut light_events = Vec::new();
1028
1029    // Parse events from logs
1030    for log in logs {
1031        if log.contains("PublicTransactionEvent") || log.contains("BatchPublicTransactionEvent") {
1032            // Parse Light Protocol events from logs
1033            light_events.push(super::types::LightProtocolEvent {
1034                event_type: "PublicTransactionEvent".to_string(),
1035                compressed_accounts: Vec::new(),
1036                merkle_tree_changes: Vec::new(),
1037                nullifiers: Vec::new(),
1038            });
1039        }
1040    }
1041
1042    light_events
1043}