light_program_test/logging/
types.rs

1//! Type definitions for enhanced logging
2
3use solana_sdk::{
4    inner_instruction::InnerInstruction, instruction::AccountMeta, pubkey::Pubkey,
5    signature::Signature, system_program,
6};
7
8use super::config::EnhancedLoggingConfig;
9
10/// Enhanced transaction log containing all formatting information
11#[derive(Debug, Clone)]
12pub struct EnhancedTransactionLog {
13    pub signature: Signature,
14    pub slot: u64,
15    pub status: TransactionStatus,
16    pub fee: u64,
17    pub compute_used: u64,
18    pub compute_total: u64,
19    pub instructions: Vec<EnhancedInstructionLog>,
20    pub account_changes: Vec<AccountChange>,
21    pub program_logs_pretty: String,
22    pub light_events: Vec<LightProtocolEvent>,
23}
24
25/// Transaction execution status
26#[derive(Debug, Clone)]
27pub enum TransactionStatus {
28    Success,
29    Failed(String),
30    Unknown,
31}
32
33impl TransactionStatus {
34    pub fn text(&self) -> String {
35        match self {
36            TransactionStatus::Success => "Success".to_string(),
37            TransactionStatus::Failed(err) => format!("Failed: {}", err),
38            TransactionStatus::Unknown => "Unknown".to_string(),
39        }
40    }
41}
42
43/// Enhanced instruction log with hierarchy and parsing
44#[derive(Debug, Clone)]
45pub struct EnhancedInstructionLog {
46    pub index: usize,
47    pub program_id: Pubkey,
48    pub program_name: String,
49    pub instruction_name: Option<String>,
50    pub accounts: Vec<AccountMeta>,
51    pub data: Vec<u8>,
52    pub parsed_data: Option<ParsedInstructionData>,
53    pub inner_instructions: Vec<EnhancedInstructionLog>,
54    pub compute_consumed: Option<u64>,
55    pub success: bool,
56    pub depth: usize,
57}
58
59/// Parsed instruction data for known programs
60#[derive(Debug, Clone)]
61pub enum ParsedInstructionData {
62    LightSystemProgram {
63        instruction_type: String,
64        compressed_accounts: Option<CompressedAccountSummary>,
65        proof_info: Option<ProofSummary>,
66        address_params: Option<Vec<AddressParam>>,
67        fee_info: Option<FeeSummary>,
68        input_account_data: Option<Vec<InputAccountData>>,
69        output_account_data: Option<Vec<OutputAccountData>>,
70    },
71    ComputeBudget {
72        instruction_type: String,
73        value: Option<u64>,
74    },
75    System {
76        instruction_type: String,
77        lamports: Option<u64>,
78        space: Option<u64>,
79        new_account: Option<Pubkey>,
80    },
81    Unknown {
82        program_name: String,
83        data_preview: String,
84    },
85}
86
87/// Summary of compressed accounts in a Light Protocol instruction
88#[derive(Debug, Clone)]
89pub struct CompressedAccountSummary {
90    pub input_accounts: usize,
91    pub output_accounts: usize,
92    pub lamports_change: Option<i64>,
93}
94
95/// Summary of proof information
96#[derive(Debug, Clone)]
97pub struct ProofSummary {
98    pub proof_type: String,
99    pub has_validity_proof: bool,
100}
101
102/// Summary of fee information
103#[derive(Debug, Clone)]
104pub struct FeeSummary {
105    pub relay_fee: Option<u64>,
106    pub compression_fee: Option<u64>,
107}
108
109/// Address assignment state
110#[derive(Debug, Clone)]
111pub enum AddressAssignment {
112    /// V1 address param (no assignment tracking)
113    V1,
114    /// Not assigned to any output account
115    None,
116    /// Assigned to output account at index
117    AssignedIndex(u8),
118}
119
120/// Address parameter information
121#[derive(Debug, Clone)]
122pub struct AddressParam {
123    pub seed: [u8; 32],
124    pub address_queue_index: Option<u8>,
125    pub address_queue_pubkey: Option<solana_sdk::pubkey::Pubkey>,
126    pub merkle_tree_index: Option<u8>,
127    pub address_merkle_tree_pubkey: Option<solana_sdk::pubkey::Pubkey>,
128    pub root_index: Option<u16>,
129    pub derived_address: Option<[u8; 32]>,
130    pub assigned_account_index: AddressAssignment,
131}
132
133/// Input account data
134#[derive(Debug, Clone)]
135pub struct InputAccountData {
136    pub lamports: u64,
137    pub owner: Option<solana_sdk::pubkey::Pubkey>,
138    pub merkle_tree_index: Option<u8>,
139    pub merkle_tree_pubkey: Option<solana_sdk::pubkey::Pubkey>,
140    pub queue_index: Option<u8>,
141    pub queue_pubkey: Option<solana_sdk::pubkey::Pubkey>,
142    pub address: Option<[u8; 32]>,
143    pub data_hash: Vec<u8>,
144    pub discriminator: Vec<u8>,
145    pub leaf_index: Option<u32>,
146    pub root_index: Option<u16>,
147}
148
149/// Output account data
150#[derive(Debug, Clone)]
151pub struct OutputAccountData {
152    pub lamports: u64,
153    pub data: Option<Vec<u8>>,
154    pub owner: Option<solana_sdk::pubkey::Pubkey>,
155    pub merkle_tree_index: Option<u8>,
156    pub merkle_tree_pubkey: Option<solana_sdk::pubkey::Pubkey>,
157    pub queue_index: Option<u8>,
158    pub queue_pubkey: Option<solana_sdk::pubkey::Pubkey>,
159    pub address: Option<[u8; 32]>,
160    pub data_hash: Vec<u8>,
161    pub discriminator: Vec<u8>,
162}
163
164/// Account state changes during transaction
165#[derive(Debug, Clone)]
166pub struct AccountChange {
167    pub pubkey: Pubkey,
168    pub account_type: String,
169    pub access: AccountAccess,
170    pub account_index: usize,
171    pub lamports_before: u64,
172    pub lamports_after: u64,
173    pub data_len_before: usize,
174    pub data_len_after: usize,
175    pub owner: Pubkey,
176    pub executable: bool,
177    pub rent_epoch: u64,
178}
179
180/// Account access pattern during transaction
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum AccountAccess {
183    Readonly,
184    Writable,
185    Signer,
186    SignerWritable,
187}
188
189impl AccountAccess {
190    pub fn symbol(&self, index: usize) -> String {
191        format!("#{}", index)
192    }
193
194    pub fn text(&self) -> &'static str {
195        match self {
196            AccountAccess::Readonly => "readonly",
197            AccountAccess::Writable => "writable",
198            AccountAccess::Signer => "signer",
199            AccountAccess::SignerWritable => "signer+writable",
200        }
201    }
202}
203
204/// Light Protocol specific events
205#[derive(Debug, Clone)]
206pub struct LightProtocolEvent {
207    pub event_type: String,
208    pub compressed_accounts: Vec<CompressedAccountInfo>,
209    pub merkle_tree_changes: Vec<MerkleTreeChange>,
210    pub nullifiers: Vec<String>,
211}
212
213/// Compressed account information
214#[derive(Debug, Clone)]
215pub struct CompressedAccountInfo {
216    pub hash: String,
217    pub owner: Pubkey,
218    pub lamports: u64,
219    pub data: Option<Vec<u8>>,
220    pub address: Option<String>,
221}
222
223/// Merkle tree state change
224#[derive(Debug, Clone)]
225pub struct MerkleTreeChange {
226    pub tree_pubkey: Pubkey,
227    pub tree_type: String,
228    pub sequence_number: u64,
229    pub leaf_index: u64,
230}
231
232impl EnhancedTransactionLog {
233    /// Use LiteSVM's pretty logs instead of parsing raw logs
234    fn get_pretty_logs_string(result: &litesvm::types::TransactionResult) -> String {
235        match result {
236            Ok(meta) => meta.pretty_logs(),
237            Err(failed) => failed.meta.pretty_logs(),
238        }
239    }
240
241    /// Create from LiteSVM transaction result
242    pub fn from_transaction_result(
243        transaction: &solana_sdk::transaction::Transaction,
244        result: &litesvm::types::TransactionResult,
245        signature: &Signature,
246        slot: u64,
247        config: &EnhancedLoggingConfig,
248    ) -> Self {
249        let (status, compute_consumed) = match result {
250            Ok(meta) => (TransactionStatus::Success, meta.compute_units_consumed),
251            Err(failed) => (
252                TransactionStatus::Failed(format!("{:?}", failed.err)),
253                failed.meta.compute_units_consumed,
254            ),
255        };
256
257        // Calculate estimated fee (basic calculation: signatures * lamports_per_signature)
258        // Default Solana fee is 5000 lamports per signature
259        let estimated_fee = (transaction.signatures.len() as u64) * 5000;
260
261        // Parse instructions
262        let instructions: Vec<EnhancedInstructionLog> = transaction
263            .message
264            .instructions
265            .iter()
266            .enumerate()
267            .map(|(index, ix)| EnhancedInstructionLog {
268                index,
269                program_id: transaction.message.account_keys[ix.program_id_index as usize],
270                program_name: get_program_name(
271                    &transaction.message.account_keys[ix.program_id_index as usize],
272                ),
273                instruction_name: None, // Will be filled by decoder
274                accounts: ix
275                    .accounts
276                    .iter()
277                    .map(|&idx| AccountMeta {
278                        pubkey: transaction.message.account_keys[idx as usize],
279                        is_signer: transaction.message.is_signer(idx as usize),
280                        is_writable: transaction.message.is_maybe_writable(idx as usize, None),
281                    })
282                    .collect(),
283                data: ix.data.clone(),
284                parsed_data: None,              // Will be filled by decoder
285                inner_instructions: Vec::new(), // Will be filled from meta
286                compute_consumed: None,
287                success: true,
288                depth: 0,
289            })
290            .collect();
291
292        // Extract inner instructions from LiteSVM metadata
293        let inner_instructions_list = match result {
294            Ok(meta) => &meta.inner_instructions,
295            Err(failed) => &failed.meta.inner_instructions,
296        };
297
298        // Apply decoder to instructions if enabled and populate inner instructions
299        let mut instructions = instructions;
300        if config.decode_light_instructions {
301            // First, decode all top-level instructions
302            for instruction in instructions.iter_mut() {
303                instruction.parsed_data = super::decoder::decode_instruction(
304                    &instruction.program_id,
305                    &instruction.data,
306                    &instruction.accounts,
307                );
308                if let Some(ref parsed) = instruction.parsed_data {
309                    instruction.instruction_name = match parsed {
310                        ParsedInstructionData::LightSystemProgram {
311                            instruction_type, ..
312                        } => Some(instruction_type.clone()),
313                        ParsedInstructionData::ComputeBudget {
314                            instruction_type, ..
315                        } => Some(instruction_type.clone()),
316                        ParsedInstructionData::System {
317                            instruction_type, ..
318                        } => Some(instruction_type.clone()),
319                        _ => None,
320                    };
321                }
322            }
323
324            // Now populate inner instructions for each top-level instruction
325            for (instruction_index, inner_list) in inner_instructions_list.iter().enumerate() {
326                if let Some(instruction) = instructions.get_mut(instruction_index) {
327                    instruction.inner_instructions = Self::parse_inner_instructions(
328                        inner_list, // inner_list is already Vec<InnerInstruction>
329                        &transaction.message.account_keys,
330                        &transaction.message, // Pass the full message for account access info
331                        1,                    // Start at depth 1 for inner instructions
332                        config,
333                    );
334                }
335            }
336        }
337
338        // Get LiteSVM's pretty formatted logs
339        let pretty_logs_string = Self::get_pretty_logs_string(result);
340
341        Self {
342            signature: *signature,
343            slot,
344            status,
345            fee: estimated_fee,
346            compute_used: compute_consumed,
347            compute_total: 1_400_000, // Default compute limit
348            instructions,
349            account_changes: Vec::new(), // Will be filled if requested
350            program_logs_pretty: pretty_logs_string,
351            light_events: Vec::new(),
352        }
353    }
354
355    /// Parse inner instructions from Solana's InnerInstruction format with proper nesting
356    fn parse_inner_instructions(
357        inner_instructions: &[InnerInstruction],
358        account_keys: &[Pubkey],
359        message: &solana_sdk::message::Message,
360        base_depth: usize,
361        config: &EnhancedLoggingConfig,
362    ) -> Vec<EnhancedInstructionLog> {
363        let mut result = Vec::new();
364
365        for (index, inner_ix) in inner_instructions.iter().enumerate() {
366            let program_id = account_keys[inner_ix.instruction.program_id_index as usize];
367            let program_name = get_program_name(&program_id);
368
369            let accounts: Vec<AccountMeta> = inner_ix
370                .instruction
371                .accounts
372                .iter()
373                .map(|&idx| {
374                    let account_index = idx as usize;
375                    let pubkey = account_keys[account_index];
376
377                    // Get the correct signer and writable information from the original transaction message
378                    let is_signer = message.is_signer(account_index);
379                    let is_writable = message.is_maybe_writable(account_index, None);
380
381                    AccountMeta {
382                        pubkey,
383                        is_signer,
384                        is_writable,
385                    }
386                })
387                .collect();
388
389            let parsed_data = if config.decode_light_instructions {
390                super::decoder::decode_instruction(
391                    &program_id,
392                    &inner_ix.instruction.data,
393                    &accounts,
394                )
395            } else {
396                None
397            };
398
399            let instruction_name = parsed_data.as_ref().and_then(|parsed| match parsed {
400                ParsedInstructionData::LightSystemProgram {
401                    instruction_type, ..
402                } => Some(instruction_type.clone()),
403                ParsedInstructionData::ComputeBudget {
404                    instruction_type, ..
405                } => Some(instruction_type.clone()),
406                ParsedInstructionData::System {
407                    instruction_type, ..
408                } => Some(instruction_type.clone()),
409                _ => None,
410            });
411
412            // Calculate the actual depth based on stack_height
413            // stack_height 2 = first level CPI (depth = base_depth + 1)
414            // stack_height 3 = second level CPI (depth = base_depth + 2), etc.
415            let instruction_depth = base_depth + (inner_ix.stack_height as usize).saturating_sub(1);
416
417            let instruction_log = EnhancedInstructionLog {
418                index,
419                program_id,
420                program_name,
421                instruction_name,
422                accounts,
423                data: inner_ix.instruction.data.clone(),
424                parsed_data,
425                inner_instructions: Vec::new(),
426                compute_consumed: None,
427                success: true, // We assume inner instructions succeeded if we're parsing them
428                depth: instruction_depth,
429            };
430
431            // Find the correct parent for this instruction based on stack height
432            // Stack height 2 = direct CPI, should be at top level
433            // Stack height 3+ = nested CPI, should be child of previous instruction with stack_height - 1
434            if inner_ix.stack_height <= 2 {
435                // Top-level CPI - add directly to result
436                result.push(instruction_log);
437            } else {
438                // Nested CPI - find the appropriate parent
439                // We need to traverse the result structure to find the right parent
440                let target_parent_depth = instruction_depth - 1;
441                if let Some(parent) =
442                    Self::find_parent_for_instruction(&mut result, target_parent_depth)
443                {
444                    parent.inner_instructions.push(instruction_log);
445                } else {
446                    // Fallback: add to top level if we can't find appropriate parent
447                    result.push(instruction_log);
448                }
449            }
450        }
451
452        result
453    }
454
455    /// Helper function to find the appropriate parent for nested instructions
456    fn find_parent_for_instruction(
457        instructions: &mut [EnhancedInstructionLog],
458        target_depth: usize,
459    ) -> Option<&mut EnhancedInstructionLog> {
460        for instruction in instructions.iter_mut().rev() {
461            if instruction.depth == target_depth {
462                return Some(instruction);
463            }
464            // Recursively search in inner instructions
465            if let Some(parent) =
466                Self::find_parent_for_instruction(&mut instruction.inner_instructions, target_depth)
467            {
468                return Some(parent);
469            }
470        }
471        None
472    }
473}
474/// Get human-readable program name from pubkey
475fn get_program_name(program_id: &Pubkey) -> String {
476    match program_id.to_string().as_str() {
477        id if id == system_program::ID.to_string() => "System Program".to_string(),
478        "ComputeBudget111111111111111111111111111111" => "Compute Budget".to_string(),
479        "SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7" => "Light System Program".to_string(),
480        "compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq" => "Account Compression".to_string(),
481        "cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m" => "Compressed Token Program".to_string(),
482        _ => format!("Unknown Program ({})", program_id),
483    }
484}