Skip to main content

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