safecoin_transaction_status/
parse_instruction.rs

1use {
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}