light_program_test/logging/
mod.rs

1//! LiteSVM integration for Light Protocol logging
2//!
3//! This module provides the glue layer between LiteSVM and the instruction-decoder crate.
4//! All logging types, decoders, and formatting utilities are in `light-instruction-decoder`.
5//!
6//! This module only contains:
7//! - LiteSVM-specific transaction result parsing (`from_transaction_result`)
8//! - Log file I/O functions
9//! - Re-exports from instruction-decoder
10
11use std::{
12    collections::HashMap,
13    fs::OpenOptions,
14    io::Write,
15    path::PathBuf,
16    time::{SystemTime, UNIX_EPOCH},
17};
18
19use chrono;
20// Re-export everything from instruction-decoder
21pub use light_instruction_decoder::{
22    AccountAccess, AccountChange, AccountCompressionInstructionDecoder, AccountStateSnapshot,
23    CTokenInstructionDecoder, Colors, CompressedAccountInfo, ComputeBudgetInstructionDecoder,
24    DecodedField, DecodedInstruction, DecoderRegistry, EnhancedInstructionLog,
25    EnhancedLoggingConfig, EnhancedTransactionLog, InstructionDecoder, LightProtocolEvent,
26    LightSystemInstructionDecoder, LogVerbosity, MerkleTreeChange, RegistryInstructionDecoder,
27    SplTokenInstructionDecoder, SystemInstructionDecoder, Token2022InstructionDecoder,
28    TransactionFormatter, TransactionStatus,
29};
30use litesvm::{types::TransactionResult, LiteSVM};
31use solana_sdk::{
32    inner_instruction::InnerInstruction, pubkey::Pubkey, signature::Signature,
33    transaction::Transaction,
34};
35
36/// Lightweight pre-transaction account state capture.
37/// Maps pubkey -> (lamports, data_len) for accounts in a transaction.
38pub type AccountStates = HashMap<Pubkey, (u64, usize)>;
39
40/// Capture account states from LiteSVM context.
41/// Call this before and after sending the transaction.
42pub fn capture_account_states(context: &LiteSVM, transaction: &Transaction) -> AccountStates {
43    let mut states = HashMap::new();
44    for pubkey in &transaction.message.account_keys {
45        if let Some(account) = context.get_account(pubkey) {
46            states.insert(*pubkey, (account.lamports, account.data.len()));
47        } else {
48            states.insert(*pubkey, (0, 0));
49        }
50    }
51    states
52}
53
54use crate::program_test::config::ProgramTestConfig;
55
56static SESSION_STARTED: std::sync::Once = std::sync::Once::new();
57
58/// Get the log file path in target directory
59fn get_log_file_path() -> PathBuf {
60    use std::process::Command;
61    if let Ok(output) = Command::new("cargo")
62        .arg("metadata")
63        .arg("--format-version=1")
64        .arg("--no-deps")
65        .output()
66    {
67        if output.status.success() {
68            if let Ok(metadata) = String::from_utf8(output.stdout) {
69                if let Ok(json) = serde_json::from_str::<serde_json::Value>(&metadata) {
70                    if let Some(target_directory) = json["target_directory"].as_str() {
71                        let mut path = PathBuf::from(target_directory);
72                        path.push("light_program_test.log");
73                        return path;
74                    }
75                }
76            }
77        }
78    }
79
80    let mut path = PathBuf::from("target");
81    path.push("light_program_test.log");
82    path
83}
84
85/// Initialize log file with session header (called only once per session)
86fn initialize_log_file() {
87    SESSION_STARTED.call_once(|| {
88        let log_path = get_log_file_path();
89        let timestamp = SystemTime::now()
90            .duration_since(UNIX_EPOCH)
91            .unwrap_or_default()
92            .as_secs();
93
94        if let Ok(mut file) = OpenOptions::new()
95            .create(true)
96            .write(true)
97            .truncate(true)
98            .open(&log_path)
99        {
100            let datetime =
101                chrono::DateTime::from_timestamp(timestamp as i64, 0).unwrap_or(chrono::Utc::now());
102            let formatted_date = datetime.format("%Y-%m-%d %H:%M:%S UTC");
103
104            let _ = writeln!(
105                file,
106                "=== Light Program Test Session Started at {} ===\n",
107                formatted_date
108            );
109        }
110    });
111}
112
113/// Strip ANSI escape codes from string for plain text log files
114fn strip_ansi_codes(text: &str) -> String {
115    let mut result = String::with_capacity(text.len());
116    let mut chars = text.chars();
117
118    while let Some(ch) = chars.next() {
119        if ch == '\x1b' {
120            for next_ch in chars.by_ref() {
121                if next_ch == 'm' {
122                    break;
123                }
124            }
125        } else {
126            result.push(ch);
127        }
128    }
129
130    result
131}
132
133/// Write log entry to file (append to existing session log)
134fn write_to_log_file(content: &str) {
135    initialize_log_file();
136
137    let log_path = get_log_file_path();
138
139    if let Some(parent) = log_path.parent() {
140        let _ = std::fs::create_dir_all(parent);
141    }
142
143    let clean_content = strip_ansi_codes(content);
144
145    if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
146        let _ = writeln!(file, "{}", clean_content);
147    }
148}
149
150/// Main entry point for enhanced transaction logging
151#[allow(clippy::too_many_arguments)]
152pub fn log_transaction_enhanced(
153    config: &ProgramTestConfig,
154    transaction: &Transaction,
155    result: &TransactionResult,
156    signature: &Signature,
157    slot: u64,
158    transaction_counter: usize,
159    pre_states: Option<&AccountStates>,
160    post_states: Option<&AccountStates>,
161) {
162    log_transaction_enhanced_with_console(
163        config,
164        transaction,
165        result,
166        signature,
167        slot,
168        transaction_counter,
169        false,
170        pre_states,
171        post_states,
172    )
173}
174
175/// Enhanced transaction logging with console output control
176#[allow(clippy::too_many_arguments)]
177pub fn log_transaction_enhanced_with_console(
178    config: &ProgramTestConfig,
179    transaction: &Transaction,
180    result: &TransactionResult,
181    signature: &Signature,
182    slot: u64,
183    transaction_counter: usize,
184    print_to_console: bool,
185    pre_states: Option<&AccountStates>,
186    post_states: Option<&AccountStates>,
187) {
188    if !config.enhanced_logging.enabled {
189        return;
190    }
191
192    let enhanced_log = from_transaction_result(
193        transaction,
194        result,
195        signature,
196        slot,
197        &config.enhanced_logging,
198        pre_states,
199        post_states,
200    );
201
202    let formatter = TransactionFormatter::new(&config.enhanced_logging);
203    let formatted_log = formatter.format(&enhanced_log, transaction_counter);
204
205    write_to_log_file(&formatted_log);
206
207    if print_to_console {
208        println!("{}", formatted_log);
209    }
210}
211
212/// Check if enhanced logging should be used instead of basic logging
213pub fn should_use_enhanced_logging(config: &ProgramTestConfig) -> bool {
214    config.enhanced_logging.enabled && !config.no_logs
215}
216
217// ============================================================================
218// LiteSVM-specific conversion functions
219// ============================================================================
220
221/// Get human-readable program name from pubkey
222fn get_program_name(program_id: &Pubkey) -> String {
223    light_instruction_decoder::types::get_program_name(
224        &solana_pubkey::Pubkey::new_from_array(program_id.to_bytes()),
225        None,
226    )
227}
228
229/// Use LiteSVM's pretty logs instead of parsing raw logs
230fn get_pretty_logs_string(result: &TransactionResult) -> String {
231    match result {
232        Ok(meta) => meta.pretty_logs(),
233        Err(failed) => failed.meta.pretty_logs(),
234    }
235}
236
237/// Create EnhancedTransactionLog from LiteSVM transaction result
238///
239/// If pre_states and post_states are provided, captures account state snapshots
240/// for all accounts in the transaction.
241///
242/// Use `capture_pre_account_states` before and after sending the transaction.
243pub fn from_transaction_result(
244    transaction: &Transaction,
245    result: &TransactionResult,
246    signature: &Signature,
247    slot: u64,
248    config: &EnhancedLoggingConfig,
249    pre_states: Option<&AccountStates>,
250    post_states: Option<&AccountStates>,
251) -> EnhancedTransactionLog {
252    let (status, compute_consumed) = match result {
253        Ok(meta) => (TransactionStatus::Success, meta.compute_units_consumed),
254        Err(failed) => (
255            TransactionStatus::Failed(format!("{:?}", failed.err)),
256            failed.meta.compute_units_consumed,
257        ),
258    };
259
260    let estimated_fee = (transaction.signatures.len() as u64) * 5000;
261
262    // Capture account states if both pre and post states are provided
263    let account_states = if let (Some(pre), Some(post)) = (pre_states, post_states) {
264        let mut states = HashMap::new();
265        for pubkey in &transaction.message.account_keys {
266            let (lamports_before, data_len_before) = pre.get(pubkey).copied().unwrap_or((0, 0));
267            let (lamports_after, data_len_after) = post.get(pubkey).copied().unwrap_or((0, 0));
268
269            states.insert(
270                solana_pubkey::Pubkey::new_from_array(pubkey.to_bytes()),
271                AccountStateSnapshot {
272                    lamports_before,
273                    lamports_after,
274                    data_len_before,
275                    data_len_after,
276                },
277            );
278        }
279        Some(states)
280    } else {
281        None
282    };
283
284    // Build full instructions with accounts and data
285    let mut instructions: Vec<EnhancedInstructionLog> = transaction
286        .message
287        .instructions
288        .iter()
289        .enumerate()
290        .map(|(index, ix)| {
291            let program_id = transaction.message.account_keys[ix.program_id_index as usize];
292            let mut log = EnhancedInstructionLog::new(
293                index,
294                solana_pubkey::Pubkey::new_from_array(program_id.to_bytes()),
295                get_program_name(&program_id),
296            );
297            log.accounts = ix
298                .accounts
299                .iter()
300                .map(|&idx| {
301                    let pubkey = transaction.message.account_keys[idx as usize];
302                    solana_instruction::AccountMeta {
303                        pubkey: solana_pubkey::Pubkey::new_from_array(pubkey.to_bytes()),
304                        is_signer: transaction.message.is_signer(idx as usize),
305                        is_writable: transaction.message.is_maybe_writable(idx as usize, None),
306                    }
307                })
308                .collect();
309            log.data = ix.data.clone();
310            log
311        })
312        .collect();
313
314    // Extract inner instructions from LiteSVM metadata
315    let inner_instructions_list = match result {
316        Ok(meta) => &meta.inner_instructions,
317        Err(failed) => &failed.meta.inner_instructions,
318    };
319
320    // Apply decoder to instructions if enabled
321    if config.decode_light_instructions {
322        for instruction in instructions.iter_mut() {
323            instruction.decode(config);
324        }
325
326        // Populate inner instructions for each top-level instruction
327        for (instruction_index, inner_list) in inner_instructions_list.iter().enumerate() {
328            if let Some(instruction) = instructions.get_mut(instruction_index) {
329                instruction.inner_instructions = parse_inner_instructions(
330                    inner_list,
331                    &transaction.message.account_keys,
332                    &transaction.message,
333                    1,
334                    config,
335                );
336            }
337        }
338    }
339
340    let pretty_logs_string = get_pretty_logs_string(result);
341
342    let sig_bytes: [u8; 64] = signature.as_ref().try_into().unwrap_or([0u8; 64]);
343    let mut log = EnhancedTransactionLog::new(
344        light_instruction_decoder::solana_signature::Signature::from(sig_bytes),
345        slot,
346    );
347    log.status = status;
348    log.fee = estimated_fee;
349    log.compute_used = compute_consumed;
350    log.instructions = instructions;
351    log.program_logs_pretty = pretty_logs_string;
352    log.account_states = account_states;
353    log
354}
355
356/// Parse inner instructions from Solana's InnerInstruction format with proper nesting
357fn parse_inner_instructions(
358    inner_instructions: &[InnerInstruction],
359    account_keys: &[Pubkey],
360    message: &solana_sdk::message::Message,
361    base_depth: usize,
362    config: &EnhancedLoggingConfig,
363) -> Vec<EnhancedInstructionLog> {
364    let mut result = Vec::new();
365
366    for (index, inner_ix) in inner_instructions.iter().enumerate() {
367        let program_id = account_keys[inner_ix.instruction.program_id_index as usize];
368        let program_name = get_program_name(&program_id);
369
370        let accounts: Vec<solana_instruction::AccountMeta> = inner_ix
371            .instruction
372            .accounts
373            .iter()
374            .map(|&idx| {
375                let account_index = idx as usize;
376                let pubkey = account_keys[account_index];
377
378                let is_signer = message.is_signer(account_index);
379                let is_writable = message.is_maybe_writable(account_index, None);
380
381                solana_instruction::AccountMeta {
382                    pubkey: solana_pubkey::Pubkey::new_from_array(pubkey.to_bytes()),
383                    is_signer,
384                    is_writable,
385                }
386            })
387            .collect();
388
389        let instruction_depth = base_depth + (inner_ix.stack_height as usize).saturating_sub(1);
390
391        let mut instruction_log = EnhancedInstructionLog::new(
392            index,
393            solana_pubkey::Pubkey::new_from_array(program_id.to_bytes()),
394            program_name,
395        );
396        instruction_log.accounts = accounts;
397        instruction_log.data = inner_ix.instruction.data.clone();
398        instruction_log.depth = instruction_depth;
399
400        // Decode the instruction if enabled
401        if config.decode_light_instructions {
402            instruction_log.decode(config);
403        }
404
405        // Find the correct parent for this instruction based on stack height
406        if inner_ix.stack_height <= 2 {
407            result.push(instruction_log);
408        } else {
409            let target_parent_depth = instruction_depth - 1;
410            if let Some(parent) = EnhancedInstructionLog::find_parent_for_instruction(
411                &mut result,
412                target_parent_depth,
413            ) {
414                parent.inner_instructions.push(instruction_log);
415            } else {
416                result.push(instruction_log);
417            }
418        }
419    }
420
421    result
422}