diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 63c374ec1e..7fbd7c3744 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -4,6 +4,7 @@ extern crate lazy_static; extern crate serde_derive; pub mod parse_accounts; +pub mod parse_bpf_loader; pub mod parse_instruction; pub mod parse_token; diff --git a/transaction-status/src/parse_bpf_loader.rs b/transaction-status/src/parse_bpf_loader.rs new file mode 100644 index 0000000000..4ca063dfdb --- /dev/null +++ b/transaction-status/src/parse_bpf_loader.rs @@ -0,0 +1,102 @@ +use crate::parse_instruction::{ParsableProgram, ParseInstructionError, ParsedInstructionEnum}; +use bincode::deserialize; +use serde_json::json; +use solana_sdk::{ + instruction::CompiledInstruction, loader_instruction::LoaderInstruction, pubkey::Pubkey, +}; + +pub fn parse_bpf_loader( + instruction: &CompiledInstruction, + account_keys: &[Pubkey], +) -> Result { + let bpf_loader_instruction: LoaderInstruction = + deserialize(&instruction.data).map_err(|err| { + println!("{:?}", err); + ParseInstructionError::InstructionNotParsable(ParsableProgram::BpfLoader) + })?; + if instruction.accounts.is_empty() || instruction.accounts[0] as usize >= account_keys.len() { + return Err(ParseInstructionError::InstructionKeyMismatch( + ParsableProgram::BpfLoader, + )); + } + match bpf_loader_instruction { + LoaderInstruction::Write { offset, bytes } => Ok(ParsedInstructionEnum { + instruction_type: "write".to_string(), + info: json!({ + "offset": offset, + "bytes": base64::encode(bytes), + "account": account_keys[instruction.accounts[0] as usize].to_string(), + }), + }), + LoaderInstruction::Finalize => Ok(ParsedInstructionEnum { + instruction_type: "finalize".to_string(), + info: json!({ + "account": account_keys[instruction.accounts[0] as usize].to_string(), + }), + }), + } +} + +#[cfg(test)] +mod test { + use super::*; + use solana_sdk::{message::Message, pubkey::Pubkey}; + + #[test] + fn test_parse_bpf_loader_instructions() { + let account_pubkey = Pubkey::new_rand(); + let program_id = Pubkey::new_rand(); + let offset = 4242; + let bytes = vec![8; 99]; + let fee_payer = Pubkey::new_rand(); + let account_keys = vec![fee_payer, account_pubkey]; + let missing_account_keys = vec![account_pubkey]; + + let instruction = solana_sdk::loader_instruction::write( + &account_pubkey, + &program_id, + offset, + bytes.clone(), + ); + let message = Message::new(&[instruction], Some(&fee_payer)); + assert_eq!( + parse_bpf_loader(&message.instructions[0], &account_keys).unwrap(), + ParsedInstructionEnum { + instruction_type: "write".to_string(), + info: json!({ + "offset": offset, + "bytes": base64::encode(&bytes), + "account": account_pubkey.to_string(), + }), + } + ); + assert!(parse_bpf_loader(&message.instructions[0], &missing_account_keys).is_err()); + + let instruction = solana_sdk::loader_instruction::finalize(&account_pubkey, &program_id); + let message = Message::new(&[instruction], Some(&fee_payer)); + assert_eq!( + parse_bpf_loader(&message.instructions[0], &account_keys).unwrap(), + ParsedInstructionEnum { + instruction_type: "finalize".to_string(), + info: json!({ + "account": account_pubkey.to_string(), + }), + } + ); + assert!(parse_bpf_loader(&message.instructions[0], &missing_account_keys).is_err()); + + let bad_compiled_instruction = CompiledInstruction { + program_id_index: 3, + accounts: vec![1, 2], + data: vec![2, 0, 0, 0], // LoaderInstruction enum only has 2 variants + }; + assert!(parse_bpf_loader(&bad_compiled_instruction, &account_keys).is_err()); + + let bad_compiled_instruction = CompiledInstruction { + program_id_index: 3, + accounts: vec![], + data: vec![1, 0, 0, 0], + }; + assert!(parse_bpf_loader(&bad_compiled_instruction, &account_keys).is_err()); + } +} diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index ebaac0922a..90e601949d 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -1,4 +1,4 @@ -use crate::parse_token::parse_token; +use crate::{parse_bpf_loader::parse_bpf_loader, parse_token::parse_token}; use inflector::Inflector; use serde_json::Value; use solana_account_decoder::parse_token::spl_token_id_v2_0; @@ -10,6 +10,7 @@ use std::{ use thiserror::Error; lazy_static! { + static ref BPF_LOADER_PROGRAM_ID: Pubkey = solana_sdk::bpf_loader::id(); static ref MEMO_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_memo_v1_0::id().to_string()).unwrap(); static ref TOKEN_PROGRAM_ID: Pubkey = spl_token_id_v2_0(); @@ -17,6 +18,7 @@ lazy_static! { let mut m = HashMap::new(); m.insert(*MEMO_PROGRAM_ID, ParsableProgram::SplMemo); m.insert(*TOKEN_PROGRAM_ID, ParsableProgram::SplToken); + m.insert(*BPF_LOADER_PROGRAM_ID, ParsableProgram::BpfLoader); m }; } @@ -49,6 +51,7 @@ pub struct ParsedInstruction { pub struct ParsedInstructionEnum { #[serde(rename = "type")] pub instruction_type: String, + #[serde(default, skip_serializing_if = "Value::is_null")] pub info: Value, } @@ -57,6 +60,7 @@ pub struct ParsedInstructionEnum { pub enum ParsableProgram { SplMemo, SplToken, + BpfLoader, } pub fn parse( @@ -70,6 +74,9 @@ pub fn parse( let parsed_json = match program_name { ParsableProgram::SplMemo => parse_memo(instruction), ParsableProgram::SplToken => serde_json::to_value(parse_token(instruction, account_keys)?)?, + ParsableProgram::BpfLoader => { + serde_json::to_value(parse_bpf_loader(instruction, account_keys)?)? + } }; Ok(ParsedInstruction { program: format!("{:?}", program_name).to_kebab_case(),