solana_transaction_status_wasm/
parse_instruction.rs

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