Skip to main content

chainerrors_solana/
log_parser.rs

1//! Parse Solana program log messages to extract error information.
2//!
3//! Solana programs emit structured log lines via `msg!()` and the runtime.
4//! Common patterns:
5//! - `"Program XYZ failed: custom program error: 0x1"`
6//! - `"Program log: Error: insufficient funds"`
7//! - `"Program log: AnchorError ... Error Code: AccountNotInitialized"`
8
9/// A parsed error from a Solana program log line.
10#[derive(Debug, Clone, PartialEq)]
11pub enum ParsedError {
12    /// A numeric error code (from `custom program error: 0xNN` or decimal).
13    Code(u32),
14    /// A text error message (from `Program log: Error: ...`).
15    Message(String),
16}
17
18/// Try to parse an error from a Solana program log line.
19///
20/// Returns `Some(ParsedError)` if a recognized error pattern is found.
21pub fn parse_program_error(log_line: &str) -> Option<ParsedError> {
22    // Pattern 1: "custom program error: 0xNN" (hex)
23    if let Some(rest) = log_line
24        .find("custom program error: 0x")
25        .map(|pos| &log_line[pos + 24..])
26    {
27        let hex_str: String = rest.chars().take_while(|c| c.is_ascii_hexdigit()).collect();
28        if !hex_str.is_empty() {
29            if let Ok(code) = u32::from_str_radix(&hex_str, 16) {
30                return Some(ParsedError::Code(code));
31            }
32        }
33    }
34
35    // Pattern 2: "custom program error: NN" (decimal)
36    if let Some(rest) = log_line
37        .find("custom program error: ")
38        .map(|pos| &log_line[pos + 22..])
39    {
40        let num_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
41        if !num_str.is_empty() {
42            if let Ok(code) = num_str.parse::<u32>() {
43                return Some(ParsedError::Code(code));
44            }
45        }
46    }
47
48    // Pattern 3: "Program log: Error: <message>"
49    if let Some(pos) = log_line.find("Program log: Error: ") {
50        let message = log_line[pos + 20..].trim().to_string();
51        if !message.is_empty() {
52            return Some(ParsedError::Message(message));
53        }
54    }
55
56    // Pattern 4: "Error Code: <name>. Error Number: <code>" (Anchor AnchorError)
57    // Check BEFORE "Error Message:" since Anchor logs contain both,
58    // and the numeric code is more precise for lookup.
59    if let Some(pos) = log_line.find("Error Number: ") {
60        let rest = &log_line[pos + 14..];
61        let num_str: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
62        if !num_str.is_empty() {
63            if let Ok(code) = num_str.parse::<u32>() {
64                return Some(ParsedError::Code(code));
65            }
66        }
67    }
68
69    // Pattern 5: "Error Message: <message>" (Anchor style, standalone)
70    if let Some(pos) = log_line.find("Error Message: ") {
71        let message = log_line[pos + 15..].trim().to_string();
72        if !message.is_empty() {
73            return Some(ParsedError::Message(message));
74        }
75    }
76
77    None
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn parse_hex_error_code() {
86        let line = "Program ABC123 failed: custom program error: 0x1";
87        assert_eq!(parse_program_error(line), Some(ParsedError::Code(1)));
88    }
89
90    #[test]
91    fn parse_hex_error_code_large() {
92        let line = "Program ABC123 failed: custom program error: 0xbc4";
93        assert_eq!(parse_program_error(line), Some(ParsedError::Code(0xbc4)));
94    }
95
96    #[test]
97    fn parse_decimal_error_code() {
98        let line = "Program failed: custom program error: 3012";
99        assert_eq!(parse_program_error(line), Some(ParsedError::Code(3012)));
100    }
101
102    #[test]
103    fn parse_error_message() {
104        let line = "Program log: Error: insufficient funds";
105        assert_eq!(
106            parse_program_error(line),
107            Some(ParsedError::Message("insufficient funds".to_string()))
108        );
109    }
110
111    #[test]
112    fn parse_anchor_error_message() {
113        let line = "Error Message: A seeds constraint was violated.";
114        assert_eq!(
115            parse_program_error(line),
116            Some(ParsedError::Message(
117                "A seeds constraint was violated.".to_string()
118            ))
119        );
120    }
121
122    #[test]
123    fn parse_anchor_error_number() {
124        let line = "Error Code: AccountNotInitialized. Error Number: 3012. Error Message: Account is not initialized.";
125        assert_eq!(parse_program_error(line), Some(ParsedError::Code(3012)));
126    }
127
128    #[test]
129    fn parse_unrecognized_returns_none() {
130        assert!(parse_program_error("Program log: some info").is_none());
131        assert!(parse_program_error("random text").is_none());
132        assert!(parse_program_error("").is_none());
133    }
134
135    #[test]
136    fn parse_zero_error_code() {
137        let line = "Program failed: custom program error: 0x0";
138        assert_eq!(parse_program_error(line), Some(ParsedError::Code(0)));
139    }
140}