solana_transaction_status/
parse_instruction.rs1pub use solana_transaction_status_client_types::ParsedInstruction;
2use {
3 crate::{
4 parse_address_lookup_table::parse_address_lookup_table,
5 parse_associated_token::parse_associated_token,
6 parse_bpf_loader::{parse_bpf_loader, parse_bpf_upgradeable_loader},
7 parse_stake::parse_stake,
8 parse_system::parse_system,
9 parse_token::parse_token,
10 parse_vote::parse_vote,
11 },
12 inflector::Inflector,
13 serde::{Deserialize, Serialize},
14 serde_json::Value,
15 solana_account_decoder::parse_token::spl_token_ids,
16 solana_message::{compiled_instruction::CompiledInstruction, AccountKeys},
17 solana_pubkey::Pubkey,
18 solana_sdk_ids::{address_lookup_table, stake, system_program, vote},
19 std::{
20 collections::HashMap,
21 str::{from_utf8, Utf8Error},
22 },
23 thiserror::Error,
24};
25
26static PARSABLE_PROGRAM_IDS: std::sync::LazyLock<HashMap<Pubkey, ParsableProgram>> =
27 std::sync::LazyLock::new(|| {
28 [
29 (
30 address_lookup_table::id(),
31 ParsableProgram::AddressLookupTable,
32 ),
33 (
34 spl_associated_token_account_interface::program::id(),
35 ParsableProgram::SplAssociatedTokenAccount,
36 ),
37 (spl_memo_interface::v1::id(), ParsableProgram::SplMemo),
38 (spl_memo_interface::v3::id(), ParsableProgram::SplMemo),
39 (solana_sdk_ids::bpf_loader::id(), ParsableProgram::BpfLoader),
40 (
41 solana_sdk_ids::bpf_loader_upgradeable::id(),
42 ParsableProgram::BpfUpgradeableLoader,
43 ),
44 (stake::id(), ParsableProgram::Stake),
45 (system_program::id(), ParsableProgram::System),
46 (vote::id(), ParsableProgram::Vote),
47 ]
48 .into_iter()
49 .chain(
50 spl_token_ids()
51 .into_iter()
52 .map(|spl_token_id| (spl_token_id, ParsableProgram::SplToken)),
53 )
54 .collect()
55 });
56
57#[derive(Error, Debug)]
58pub enum ParseInstructionError {
59 #[error("{0:?} instruction not parsable")]
60 InstructionNotParsable(ParsableProgram),
61
62 #[error("{0:?} instruction key mismatch")]
63 InstructionKeyMismatch(ParsableProgram),
64
65 #[error("Program not parsable")]
66 ProgramNotParsable,
67
68 #[error("Internal error, please report")]
69 SerdeJsonError(#[from] serde_json::error::Error),
70}
71
72#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
73#[serde(rename_all = "camelCase")]
74pub struct ParsedInstructionEnum {
75 #[serde(rename = "type")]
76 pub instruction_type: String,
77 #[serde(default, skip_serializing_if = "Value::is_null")]
78 pub info: Value,
79}
80
81#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(rename_all = "camelCase")]
83pub enum ParsableProgram {
84 AddressLookupTable,
85 SplAssociatedTokenAccount,
86 SplMemo,
87 SplToken,
88 BpfLoader,
89 BpfUpgradeableLoader,
90 Stake,
91 System,
92 Vote,
93}
94
95pub fn parse(
96 program_id: &Pubkey,
97 instruction: &CompiledInstruction,
98 account_keys: &AccountKeys,
99 stack_height: Option<u32>,
100) -> Result<ParsedInstruction, ParseInstructionError> {
101 let program_name = PARSABLE_PROGRAM_IDS
102 .get(program_id)
103 .ok_or(ParseInstructionError::ProgramNotParsable)?;
104 let parsed_json = match program_name {
105 ParsableProgram::AddressLookupTable => {
106 serde_json::to_value(parse_address_lookup_table(instruction, account_keys)?)?
107 }
108 ParsableProgram::SplAssociatedTokenAccount => {
109 serde_json::to_value(parse_associated_token(instruction, account_keys)?)?
110 }
111 ParsableProgram::SplMemo => parse_memo(instruction)?,
112 ParsableProgram::SplToken => serde_json::to_value(parse_token(instruction, account_keys)?)?,
113 ParsableProgram::BpfLoader => {
114 serde_json::to_value(parse_bpf_loader(instruction, account_keys)?)?
115 }
116 ParsableProgram::BpfUpgradeableLoader => {
117 serde_json::to_value(parse_bpf_upgradeable_loader(instruction, account_keys)?)?
118 }
119 ParsableProgram::Stake => serde_json::to_value(parse_stake(instruction, account_keys)?)?,
120 ParsableProgram::System => serde_json::to_value(parse_system(instruction, account_keys)?)?,
121 ParsableProgram::Vote => serde_json::to_value(parse_vote(instruction, account_keys)?)?,
122 };
123 Ok(ParsedInstruction {
124 program: format!("{program_name:?}").to_kebab_case(),
125 program_id: program_id.to_string(),
126 parsed: parsed_json,
127 stack_height,
128 })
129}
130
131fn parse_memo(instruction: &CompiledInstruction) -> Result<Value, ParseInstructionError> {
132 parse_memo_data(&instruction.data)
133 .map(Value::String)
134 .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::SplMemo))
135}
136
137pub fn parse_memo_data(data: &[u8]) -> Result<String, Utf8Error> {
138 from_utf8(data).map(|s| s.to_string())
139}
140
141pub(crate) fn check_num_accounts(
142 accounts: &[u8],
143 num: usize,
144 parsable_program: ParsableProgram,
145) -> Result<(), ParseInstructionError> {
146 if accounts.len() < num {
147 Err(ParseInstructionError::InstructionKeyMismatch(
148 parsable_program,
149 ))
150 } else {
151 Ok(())
152 }
153}
154
155#[cfg(test)]
156mod test {
157 use {super::*, serde_json::json};
158
159 #[test]
160 fn test_parse() {
161 let no_keys = AccountKeys::new(&[], None);
162 let memo_instruction = CompiledInstruction {
163 program_id_index: 0,
164 accounts: vec![],
165 data: vec![240, 159, 166, 150],
166 };
167 assert_eq!(
168 parse(
169 &spl_memo_interface::v1::id(),
170 &memo_instruction,
171 &no_keys,
172 None
173 )
174 .unwrap(),
175 ParsedInstruction {
176 program: "spl-memo".to_string(),
177 program_id: spl_memo_interface::v1::id().to_string(),
178 parsed: json!("🦖"),
179 stack_height: None,
180 }
181 );
182 assert_eq!(
183 parse(
184 &spl_memo_interface::v3::id(),
185 &memo_instruction,
186 &no_keys,
187 Some(1)
188 )
189 .unwrap(),
190 ParsedInstruction {
191 program: "spl-memo".to_string(),
192 program_id: spl_memo_interface::v3::id().to_string(),
193 parsed: json!("🦖"),
194 stack_height: Some(1),
195 }
196 );
197
198 let non_parsable_program_id = Pubkey::from([1; 32]);
199 assert!(parse(&non_parsable_program_id, &memo_instruction, &no_keys, None).is_err());
200 }
201
202 #[test]
203 fn test_parse_memo() {
204 let good_memo = "good memo".to_string();
205 assert_eq!(
206 parse_memo(&CompiledInstruction {
207 program_id_index: 0,
208 accounts: vec![],
209 data: good_memo.as_bytes().to_vec(),
210 })
211 .unwrap(),
212 Value::String(good_memo),
213 );
214
215 let bad_memo = vec![128u8];
216 assert!(std::str::from_utf8(&bad_memo).is_err());
217 assert!(parse_memo(&CompiledInstruction {
218 program_id_index: 0,
219 data: bad_memo,
220 accounts: vec![],
221 })
222 .is_err(),);
223 }
224}