Skip to main content

chainerrors_solana/
lib.rs

1//! chainerrors-solana — Solana program error decoder.
2//!
3//! Decodes errors from Solana programs including:
4//! - System program errors (codes 0-17)
5//! - SPL Token program errors
6//! - Anchor framework errors (100-5100+ range)
7//! - Custom program error codes from transaction logs
8//!
9//! # Usage
10//! ```rust
11//! use chainerrors_solana::SolanaErrorDecoder;
12//! use chainerrors_core::ErrorDecoder;
13//!
14//! let decoder = SolanaErrorDecoder::new();
15//! // Decode a system program error code
16//! let result = decoder.decode_error_code(1, None, None).unwrap();
17//! println!("{result}");
18//! ```
19
20mod system_errors;
21mod token_errors;
22mod anchor_errors;
23mod log_parser;
24
25pub use log_parser::parse_program_error;
26
27use chainerrors_core::decoder::{DecodeErrorError, ErrorDecoder};
28use chainerrors_core::types::{DecodedError, ErrorContext, ErrorKind, ErrorFieldValue};
29
30/// Solana error decoder.
31///
32/// Decodes raw error data from Solana program failures. Unlike EVM where
33/// errors are ABI-encoded bytes, Solana errors come in several forms:
34/// - Numeric error codes in transaction results
35/// - Program log messages (`"Program failed: custom program error: 0x..."`)
36/// - Instruction error enums
37pub struct SolanaErrorDecoder;
38
39impl SolanaErrorDecoder {
40    /// Create a new Solana error decoder.
41    pub fn new() -> Self {
42        Self
43    }
44
45    /// Decode a Solana error from a numeric error code.
46    ///
47    /// `program_id` helps determine which error table to use:
48    /// - `None` or `"11111111111111111111111111111111"` → System program
49    /// - `"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"` → SPL Token
50    /// - Other → try Anchor error codes, then fall back to generic
51    pub fn decode_error_code(
52        &self,
53        code: u32,
54        program_id: Option<&str>,
55        ctx: Option<ErrorContext>,
56    ) -> Result<DecodedError, DecodeErrorError> {
57        // Try program-specific errors first
58        if let Some(program) = program_id {
59            if program == "11111111111111111111111111111111" {
60                if let Some((name, desc)) = system_errors::lookup(code) {
61                    return Ok(DecodedError {
62                        kind: ErrorKind::CustomError {
63                            name: name.to_string(),
64                            inputs: vec![
65                                ("code".to_string(), ErrorFieldValue::Uint(code as u128)),
66                            ],
67                        },
68                        raw_data: code.to_le_bytes().to_vec(),
69                        selector: None,
70                        suggestion: Some(desc.to_string()),
71                        confidence: 1.0,
72                        context: ctx,
73                    });
74                }
75            }
76
77            // SPL Token program
78            if program == "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
79                || program == "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
80            {
81                if let Some((name, desc)) = token_errors::lookup(code) {
82                    return Ok(DecodedError {
83                        kind: ErrorKind::CustomError {
84                            name: name.to_string(),
85                            inputs: vec![
86                                ("code".to_string(), ErrorFieldValue::Uint(code as u128)),
87                            ],
88                        },
89                        raw_data: code.to_le_bytes().to_vec(),
90                        selector: None,
91                        suggestion: Some(desc.to_string()),
92                        confidence: 1.0,
93                        context: ctx,
94                    });
95                }
96            }
97        }
98
99        // Try Anchor error codes (100-5100+ range)
100        if let Some((name, desc)) = anchor_errors::lookup(code) {
101            return Ok(DecodedError {
102                kind: ErrorKind::CustomError {
103                    name: name.to_string(),
104                    inputs: vec![
105                        ("code".to_string(), ErrorFieldValue::Uint(code as u128)),
106                    ],
107                },
108                raw_data: code.to_le_bytes().to_vec(),
109                selector: None,
110                suggestion: Some(desc.to_string()),
111                confidence: 0.9,
112                context: ctx,
113            });
114        }
115
116        // Try system program errors without program_id context
117        if let Some((name, desc)) = system_errors::lookup(code) {
118            return Ok(DecodedError {
119                kind: ErrorKind::CustomError {
120                    name: name.to_string(),
121                    inputs: vec![
122                        ("code".to_string(), ErrorFieldValue::Uint(code as u128)),
123                    ],
124                },
125                raw_data: code.to_le_bytes().to_vec(),
126                selector: None,
127                suggestion: Some(desc.to_string()),
128                confidence: 0.7,
129                context: ctx,
130            });
131        }
132
133        // Unknown error code
134        Ok(DecodedError {
135            kind: ErrorKind::CustomError {
136                name: "UnknownProgramError".to_string(),
137                inputs: vec![
138                    ("code".to_string(), ErrorFieldValue::Uint(code as u128)),
139                    ("program".to_string(), ErrorFieldValue::Str(
140                        program_id.unwrap_or("unknown").to_string(),
141                    )),
142                ],
143            },
144            raw_data: code.to_le_bytes().to_vec(),
145            selector: None,
146            suggestion: Some(format!(
147                "Unknown Solana program error code {code} (0x{code:04x})."
148            )),
149            confidence: 0.1,
150            context: ctx,
151        })
152    }
153
154    /// Decode a Solana error from a program log line.
155    ///
156    /// Parses patterns like:
157    /// - `"Program failed: custom program error: 0x1"` → error code 1
158    /// - `"Program log: Error: insufficient funds"` → revert string
159    pub fn decode_log(
160        &self,
161        log_line: &str,
162        ctx: Option<ErrorContext>,
163    ) -> Result<DecodedError, DecodeErrorError> {
164        if let Some(parsed) = log_parser::parse_program_error(log_line) {
165            match parsed {
166                log_parser::ParsedError::Code(code) => {
167                    self.decode_error_code(code, None, ctx)
168                }
169                log_parser::ParsedError::Message(msg) => {
170                    Ok(DecodedError {
171                        kind: ErrorKind::RevertString {
172                            message: msg.clone(),
173                        },
174                        raw_data: msg.as_bytes().to_vec(),
175                        selector: None,
176                        suggestion: None,
177                        confidence: 0.8,
178                        context: ctx,
179                    })
180                }
181            }
182        } else {
183            Ok(DecodedError::empty(log_line.as_bytes().to_vec()))
184        }
185    }
186}
187
188impl Default for SolanaErrorDecoder {
189    fn default() -> Self {
190        Self::new()
191    }
192}
193
194impl ErrorDecoder for SolanaErrorDecoder {
195    fn chain_family(&self) -> &'static str {
196        "solana"
197    }
198
199    fn decode(
200        &self,
201        revert_data: &[u8],
202        ctx: Option<ErrorContext>,
203    ) -> Result<DecodedError, DecodeErrorError> {
204        if revert_data.is_empty() {
205            return Ok(DecodedError::empty(vec![]));
206        }
207
208        // Try to interpret as a UTF-8 log line
209        if let Ok(log_line) = std::str::from_utf8(revert_data) {
210            let result = self.decode_log(log_line, ctx.clone())?;
211            if result.confidence > 0.0 {
212                return Ok(result);
213            }
214        }
215
216        // Try to interpret as a 4-byte little-endian error code
217        if revert_data.len() == 4 {
218            let code = u32::from_le_bytes(revert_data.try_into().unwrap());
219            return self.decode_error_code(code, None, ctx);
220        }
221
222        // Unknown format
223        Ok(DecodedError {
224            kind: ErrorKind::RawRevert {
225                selector: if revert_data.len() >= 4 {
226                    hex::encode(&revert_data[..4])
227                } else {
228                    hex::encode(revert_data)
229                },
230                data: revert_data.to_vec(),
231            },
232            raw_data: revert_data.to_vec(),
233            selector: None,
234            suggestion: Some("Unknown Solana error data format.".into()),
235            confidence: 0.0,
236            context: ctx,
237        })
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use chainerrors_core::ErrorDecoder;
245
246    #[test]
247    fn decode_system_error() {
248        let decoder = SolanaErrorDecoder::new();
249        let result = decoder
250            .decode_error_code(1, Some("11111111111111111111111111111111"), None)
251            .unwrap();
252        assert!(result.is_decoded());
253        match &result.kind {
254            ErrorKind::CustomError { name, .. } => {
255                assert_eq!(name, "AccountAlreadyInUse");
256            }
257            _ => panic!("expected CustomError, got {:?}", result.kind),
258        }
259    }
260
261    #[test]
262    fn decode_spl_token_error() {
263        let decoder = SolanaErrorDecoder::new();
264        let result = decoder
265            .decode_error_code(
266                1,
267                Some("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),
268                None,
269            )
270            .unwrap();
271        assert!(result.is_decoded());
272        match &result.kind {
273            ErrorKind::CustomError { name, .. } => {
274                assert_eq!(name, "InsufficientFunds");
275            }
276            _ => panic!("expected CustomError, got {:?}", result.kind),
277        }
278    }
279
280    #[test]
281    fn decode_anchor_error() {
282        let decoder = SolanaErrorDecoder::new();
283        // Anchor AccountNotInitialized = 3012
284        let result = decoder
285            .decode_error_code(3012, None, None)
286            .unwrap();
287        assert!(result.confidence >= 0.8);
288        match &result.kind {
289            ErrorKind::CustomError { name, .. } => {
290                assert_eq!(name, "AccountNotInitialized");
291            }
292            _ => panic!("expected CustomError, got {:?}", result.kind),
293        }
294    }
295
296    #[test]
297    fn decode_unknown_code() {
298        let decoder = SolanaErrorDecoder::new();
299        let result = decoder
300            .decode_error_code(99999, None, None)
301            .unwrap();
302        assert!(!result.is_decoded());
303        match &result.kind {
304            ErrorKind::CustomError { name, .. } => {
305                assert_eq!(name, "UnknownProgramError");
306            }
307            _ => panic!("expected CustomError"),
308        }
309    }
310
311    #[test]
312    fn decode_from_log_hex_code() {
313        let decoder = SolanaErrorDecoder::new();
314        let result = decoder
315            .decode_log("Program failed: custom program error: 0x1", None)
316            .unwrap();
317        assert!(result.confidence > 0.0);
318    }
319
320    #[test]
321    fn decode_from_log_message() {
322        let decoder = SolanaErrorDecoder::new();
323        let result = decoder
324            .decode_log("Program log: Error: insufficient funds", None)
325            .unwrap();
326        match &result.kind {
327            ErrorKind::RevertString { message } => {
328                assert_eq!(message, "insufficient funds");
329            }
330            _ => panic!("expected RevertString, got {:?}", result.kind),
331        }
332    }
333
334    #[test]
335    fn decode_empty_bytes() {
336        let decoder = SolanaErrorDecoder::new();
337        let result = decoder.decode(&[], None).unwrap();
338        assert!(matches!(result.kind, ErrorKind::Empty));
339    }
340
341    #[test]
342    fn decode_bytes_as_log() {
343        let decoder = SolanaErrorDecoder::new();
344        let log = b"Program failed: custom program error: 0x0";
345        let result = decoder.decode(log, None).unwrap();
346        assert!(result.confidence > 0.0);
347    }
348
349    #[test]
350    fn decode_4byte_error_code() {
351        let decoder = SolanaErrorDecoder::new();
352        let code: u32 = 1;
353        let result = decoder.decode(&code.to_le_bytes(), None).unwrap();
354        assert!(result.confidence > 0.0);
355    }
356
357    #[test]
358    fn chain_family_is_solana() {
359        let decoder = SolanaErrorDecoder::new();
360        assert_eq!(decoder.chain_family(), "solana");
361    }
362
363    #[test]
364    fn decode_hex_helper_works() {
365        let decoder = SolanaErrorDecoder::new();
366        // 4 bytes LE = error code 1
367        let result = decoder.decode_hex("01000000", None).unwrap();
368        assert!(result.confidence > 0.0);
369    }
370
371    #[test]
372    fn decode_log_decimal_code() {
373        let decoder = SolanaErrorDecoder::new();
374        let result = decoder
375            .decode_log("Program failed: custom program error: 3012", None)
376            .unwrap();
377        match &result.kind {
378            ErrorKind::CustomError { name, .. } => {
379                assert_eq!(name, "AccountNotInitialized");
380            }
381            _ => panic!("expected CustomError, got {:?}", result.kind),
382        }
383    }
384
385    #[test]
386    fn decode_spl_token_2022_error() {
387        let decoder = SolanaErrorDecoder::new();
388        let result = decoder
389            .decode_error_code(
390                0,
391                Some("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"),
392                None,
393            )
394            .unwrap();
395        assert!(result.is_decoded());
396        match &result.kind {
397            ErrorKind::CustomError { name, .. } => {
398                assert_eq!(name, "NotRentExempt");
399            }
400            _ => panic!("expected CustomError"),
401        }
402    }
403
404    #[test]
405    fn decode_system_all_codes() {
406        let decoder = SolanaErrorDecoder::new();
407        let system = "11111111111111111111111111111111";
408        // Verify all 18 system errors are decodable (0-17)
409        for code in 0..=17 {
410            let result = decoder
411                .decode_error_code(code, Some(system), None)
412                .unwrap();
413            assert!(result.is_decoded(), "system error code {code} should decode");
414        }
415    }
416
417    #[test]
418    fn decode_token_all_codes() {
419        let decoder = SolanaErrorDecoder::new();
420        let token = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
421        // Verify all 18 token errors are decodable (0-17)
422        for code in 0..=17 {
423            let result = decoder
424                .decode_error_code(code, Some(token), None)
425                .unwrap();
426            assert!(result.is_decoded(), "token error code {code} should decode");
427        }
428    }
429
430    #[test]
431    fn decode_unrecognized_log() {
432        let decoder = SolanaErrorDecoder::new();
433        let result = decoder
434            .decode_log("some random log line", None)
435            .unwrap();
436        assert!(matches!(result.kind, ErrorKind::Empty));
437    }
438
439    #[test]
440    fn decode_context_preserved() {
441        let decoder = SolanaErrorDecoder::new();
442        let ctx = ErrorContext {
443            chain: Some("solana".to_string()),
444            tx_hash: Some("5abc...".to_string()),
445            contract_address: None,
446            call_selector: None,
447            block_number: Some(200_000_000),
448        };
449        let result = decoder
450            .decode_error_code(1, Some("11111111111111111111111111111111"), Some(ctx))
451            .unwrap();
452        assert_eq!(result.context.as_ref().unwrap().chain.as_deref(), Some("solana"));
453        assert_eq!(result.context.as_ref().unwrap().block_number, Some(200_000_000));
454    }
455}