1mod 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
30pub struct SolanaErrorDecoder;
38
39impl SolanaErrorDecoder {
40 pub fn new() -> Self {
42 Self
43 }
44
45 pub fn decode_error_code(
52 &self,
53 code: u32,
54 program_id: Option<&str>,
55 ctx: Option<ErrorContext>,
56 ) -> Result<DecodedError, DecodeErrorError> {
57 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 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 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 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 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 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 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 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 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 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 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 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 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}