solana_transaction_status/
parse_instruction.rs

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