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