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