solana_transaction_status/
parse_instruction.rs

1pub 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}