|
4 | 4 | use serde_derive::{Deserialize, Serialize};
|
5 | 5 | #[cfg(feature = "frozen-abi")]
|
6 | 6 | use solana_frozen_abi_macro::{AbiEnumVisitor, AbiExample};
|
7 |
| -use {core::fmt, solana_instruction::error::InstructionError, solana_sanitize::SanitizeError}; |
| 7 | +use { |
| 8 | + core::fmt, solana_instruction::error::InstructionError, solana_pubkey::Pubkey, |
| 9 | + solana_sanitize::SanitizeError, |
| 10 | +}; |
8 | 11 |
|
9 | 12 | pub type TransactionResult<T> = Result<T, TransactionError>;
|
10 | 13 |
|
@@ -42,9 +45,23 @@ pub enum TransactionError {
|
42 | 45 | /// the `recent_blockhash` has been discarded.
|
43 | 46 | BlockhashNotFound,
|
44 | 47 |
|
45 |
| - /// An error occurred while processing an instruction. The first element of the tuple |
46 |
| - /// indicates the instruction index in which the error occurred. |
47 |
| - InstructionError(u8, InstructionError), |
| 48 | + /// An error occurred while processing an instruction. |
| 49 | + InstructionError { |
| 50 | + err: InstructionError, |
| 51 | + /// The index of the inner instruction in which the error was thrown, starting from zero. |
| 52 | + /// This value will be `None` when the error was thrown from the outer instruction's |
| 53 | + /// program, and also for all errors stored using a version of this enum variant prior to |
| 54 | + /// `solana-transaction-error` 3.0.0. |
| 55 | + inner_instruction_index: Option<u8>, |
| 56 | + /// The index of the outer instruction in which the error was thrown, starting from zero. Do |
| 57 | + /// not infer the responsible program from the instruction at this index; the error might |
| 58 | + /// have been thrown from one of its inner instructions. See `responsible_program_address`. |
| 59 | + outer_instruction_index: u8, |
| 60 | + /// The address of the program that threw the error. Use this to decode the error (eg. to |
| 61 | + /// look up a custom error code in the program's IDL). This value will be `None` for errors |
| 62 | + /// stored using a version of this enum variant prior to `solana-transaction-error` 3.0.0. |
| 63 | + responsible_program_address: Option<Pubkey>, |
| 64 | + }, |
48 | 65 |
|
49 | 66 | /// Loader call chain is too deep
|
50 | 67 | CallChainTooDeep,
|
@@ -163,7 +180,17 @@ impl fmt::Display for TransactionError {
|
163 | 180 | => f.write_str("This transaction has already been processed"),
|
164 | 181 | Self::BlockhashNotFound
|
165 | 182 | => f.write_str("Blockhash not found"),
|
166 |
| - Self::InstructionError(idx, err) => write!(f, "Error processing Instruction {idx}: {err}"), |
| 183 | + Self::InstructionError { |
| 184 | + err, |
| 185 | + outer_instruction_index, |
| 186 | + .. |
| 187 | + } |
| 188 | + // NOTE: We intentionally do not augment the error message in the event that the error |
| 189 | + // carries the address of the responsible program or the index of the inner |
| 190 | + // instruction. While it would add value to the log, to do so at this point would also |
| 191 | + // break any log parser that presumes a stable log format |
| 192 | + // (eg. https://tinyurl.com/3uuczr68). |
| 193 | + => write!(f, "Error processing Instruction {outer_instruction_index}: {err}"), |
167 | 194 | Self::CallChainTooDeep
|
168 | 195 | => f.write_str("Loader call chain is too deep"),
|
169 | 196 | Self::MissingSignatureForFee
|
@@ -415,3 +442,78 @@ impl TransportError {
|
415 | 442 |
|
416 | 443 | #[cfg(not(target_os = "solana"))]
|
417 | 444 | pub type TransportResult<T> = std::result::Result<T, TransportError>;
|
| 445 | + |
| 446 | +#[cfg(feature = "serde")] |
| 447 | +#[cfg(test)] |
| 448 | +mod tests { |
| 449 | + use { |
| 450 | + crate::TransactionError, |
| 451 | + serde_json::{from_value, json, to_value}, |
| 452 | + solana_instruction::error::InstructionError, |
| 453 | + solana_pubkey::Pubkey, |
| 454 | + test_case::test_case, |
| 455 | + }; |
| 456 | + |
| 457 | + #[cfg(feature = "serde")] |
| 458 | + #[test_case(InstructionError::Custom(42), 1, Some(Pubkey::new_unique()), None; "From top-level instruction")] |
| 459 | + #[test_case(InstructionError::Custom(42), 1, Some(Pubkey::new_unique()), Some(41); "From inner instruction")] |
| 460 | + #[test_case(InstructionError::Custom(42), 1, None, None; "Legacy instruction without inner/program")] |
| 461 | + fn test_serialize_instruction_error( |
| 462 | + err: InstructionError, |
| 463 | + outer_instruction_index: u8, |
| 464 | + responsible_program_address: Option<Pubkey>, |
| 465 | + inner_instruction_index: Option<u8>, |
| 466 | + ) { |
| 467 | + let json = to_value(TransactionError::InstructionError { |
| 468 | + err: err.clone(), |
| 469 | + inner_instruction_index, |
| 470 | + outer_instruction_index, |
| 471 | + responsible_program_address, |
| 472 | + }) |
| 473 | + .unwrap(); |
| 474 | + |
| 475 | + assert_eq!( |
| 476 | + json, |
| 477 | + json!({ |
| 478 | + "InstructionError": { |
| 479 | + "err": err, |
| 480 | + "inner_instruction_index": inner_instruction_index, |
| 481 | + "outer_instruction_index": outer_instruction_index, |
| 482 | + "responsible_program_address": responsible_program_address, |
| 483 | + }, |
| 484 | + }), |
| 485 | + ) |
| 486 | + } |
| 487 | + |
| 488 | + #[cfg(feature = "serde")] |
| 489 | + #[test_case(InstructionError::Custom(42), 1, Some(Pubkey::new_unique()), None; "From top-level instruction")] |
| 490 | + #[test_case(InstructionError::Custom(42), 1, Some(Pubkey::new_unique()), Some(41); "From inner instruction")] |
| 491 | + #[test_case(InstructionError::Custom(42), 1, None, None; "Legacy instruction without inner/program")] |
| 492 | + #[test_case(InstructionError::Custom(42), 1, None, Some(41); "Mixed instruction with inner index but no program address")] |
| 493 | + fn test_deserialize_instruction_error( |
| 494 | + err: InstructionError, |
| 495 | + outer_instruction_index: u8, |
| 496 | + responsible_program_address: Option<Pubkey>, |
| 497 | + inner_instruction_index: Option<u8>, |
| 498 | + ) { |
| 499 | + let decoded_err: TransactionError = from_value(json!({ |
| 500 | + "InstructionError": { |
| 501 | + "err": err, |
| 502 | + "inner_instruction_index": inner_instruction_index, |
| 503 | + "outer_instruction_index": outer_instruction_index, |
| 504 | + "responsible_program_address": responsible_program_address, |
| 505 | + }, |
| 506 | + })) |
| 507 | + .unwrap(); |
| 508 | + |
| 509 | + assert_eq!( |
| 510 | + decoded_err, |
| 511 | + TransactionError::InstructionError { |
| 512 | + err, |
| 513 | + inner_instruction_index, |
| 514 | + outer_instruction_index, |
| 515 | + responsible_program_address, |
| 516 | + }, |
| 517 | + ) |
| 518 | + } |
| 519 | +} |
0 commit comments