solana_transaction_status/
parse_address_lookup_table.rs

1use {
2    crate::parse_instruction::{
3        check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum,
4    },
5    bincode::deserialize,
6    serde_json::json,
7    solana_address_lookup_table_interface::instruction::ProgramInstruction,
8    solana_message::{compiled_instruction::CompiledInstruction, AccountKeys},
9};
10
11pub fn parse_address_lookup_table(
12    instruction: &CompiledInstruction,
13    account_keys: &AccountKeys,
14) -> Result<ParsedInstructionEnum, ParseInstructionError> {
15    let address_lookup_table_instruction: ProgramInstruction = deserialize(&instruction.data)
16        .map_err(|_| {
17            ParseInstructionError::InstructionNotParsable(ParsableProgram::AddressLookupTable)
18        })?;
19    match instruction.accounts.iter().max() {
20        Some(index) if (*index as usize) < account_keys.len() => {}
21        _ => {
22            // Runtime should prevent this from ever happening
23            return Err(ParseInstructionError::InstructionKeyMismatch(
24                ParsableProgram::AddressLookupTable,
25            ));
26        }
27    }
28    match address_lookup_table_instruction {
29        ProgramInstruction::CreateLookupTable {
30            recent_slot,
31            bump_seed,
32        } => {
33            check_num_address_lookup_table_accounts(&instruction.accounts, 4)?;
34            Ok(ParsedInstructionEnum {
35                instruction_type: "createLookupTable".to_string(),
36                info: json!({
37                    "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(),
38                    "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(),
39                    "payerAccount": account_keys[instruction.accounts[2] as usize].to_string(),
40                    "systemProgram": account_keys[instruction.accounts[3] as usize].to_string(),
41                    "recentSlot": recent_slot,
42                    "bumpSeed": bump_seed,
43                }),
44            })
45        }
46        ProgramInstruction::FreezeLookupTable => {
47            check_num_address_lookup_table_accounts(&instruction.accounts, 2)?;
48            Ok(ParsedInstructionEnum {
49                instruction_type: "freezeLookupTable".to_string(),
50                info: json!({
51                    "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(),
52                    "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(),
53                }),
54            })
55        }
56        ProgramInstruction::ExtendLookupTable { new_addresses } => {
57            check_num_address_lookup_table_accounts(&instruction.accounts, 2)?;
58            let new_addresses: Vec<String> = new_addresses
59                .into_iter()
60                .map(|address| address.to_string())
61                .collect();
62            let mut value = json!({
63                "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(),
64                "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(),
65                "newAddresses": new_addresses,
66            });
67            let map = value.as_object_mut().unwrap();
68            if instruction.accounts.len() >= 4 {
69                map.insert(
70                    "payerAccount".to_string(),
71                    json!(account_keys[instruction.accounts[2] as usize].to_string()),
72                );
73                map.insert(
74                    "systemProgram".to_string(),
75                    json!(account_keys[instruction.accounts[3] as usize].to_string()),
76                );
77            }
78            Ok(ParsedInstructionEnum {
79                instruction_type: "extendLookupTable".to_string(),
80                info: value,
81            })
82        }
83        ProgramInstruction::DeactivateLookupTable => {
84            check_num_address_lookup_table_accounts(&instruction.accounts, 2)?;
85            Ok(ParsedInstructionEnum {
86                instruction_type: "deactivateLookupTable".to_string(),
87                info: json!({
88                    "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(),
89                    "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(),
90                }),
91            })
92        }
93        ProgramInstruction::CloseLookupTable => {
94            check_num_address_lookup_table_accounts(&instruction.accounts, 3)?;
95            Ok(ParsedInstructionEnum {
96                instruction_type: "closeLookupTable".to_string(),
97                info: json!({
98                    "lookupTableAccount": account_keys[instruction.accounts[0] as usize].to_string(),
99                    "lookupTableAuthority": account_keys[instruction.accounts[1] as usize].to_string(),
100                    "recipient": account_keys[instruction.accounts[2] as usize].to_string(),
101                }),
102            })
103        }
104    }
105}
106
107fn check_num_address_lookup_table_accounts(
108    accounts: &[u8],
109    num: usize,
110) -> Result<(), ParseInstructionError> {
111    check_num_accounts(accounts, num, ParsableProgram::AddressLookupTable)
112}
113
114#[cfg(test)]
115mod test {
116    use {
117        super::*, solana_address_lookup_table_interface::instruction, solana_message::Message,
118        solana_pubkey::Pubkey, solana_sdk_ids::system_program, std::str::FromStr,
119    };
120
121    #[test]
122    fn test_parse_create_address_lookup_table_ix() {
123        let from_pubkey = Pubkey::new_unique();
124        // use explicit key to have predictable bump_seed
125        let authority = Pubkey::from_str("HkxY6vXdrKzoCQLmdJ3cYo9534FdZQxzBNWTyrJzzqJM").unwrap();
126        let slot = 42;
127
128        let (instruction, lookup_table_pubkey) =
129            instruction::create_lookup_table(authority, from_pubkey, slot);
130        let mut message = Message::new(&[instruction], None);
131        assert_eq!(
132            parse_address_lookup_table(
133                &message.instructions[0],
134                &AccountKeys::new(&message.account_keys, None)
135            )
136            .unwrap(),
137            ParsedInstructionEnum {
138                instruction_type: "createLookupTable".to_string(),
139                info: json!({
140                    "lookupTableAccount": lookup_table_pubkey.to_string(),
141                    "lookupTableAuthority": authority.to_string(),
142                    "payerAccount": from_pubkey.to_string(),
143                    "systemProgram": system_program::id().to_string(),
144                    "recentSlot": slot,
145                    "bumpSeed": 254,
146                }),
147            }
148        );
149        assert!(parse_address_lookup_table(
150            &message.instructions[0],
151            &AccountKeys::new(&message.account_keys[0..3], None)
152        )
153        .is_err());
154        let keys = message.account_keys.clone();
155        message.instructions[0].accounts.pop();
156        assert!(parse_address_lookup_table(
157            &message.instructions[0],
158            &AccountKeys::new(&keys, None)
159        )
160        .is_err());
161    }
162
163    #[test]
164    fn test_parse_freeze_lookup_table_ix() {
165        let lookup_table_pubkey = Pubkey::new_unique();
166        let authority = Pubkey::new_unique();
167
168        let instruction = instruction::freeze_lookup_table(lookup_table_pubkey, authority);
169        let mut message = Message::new(&[instruction], None);
170        assert_eq!(
171            parse_address_lookup_table(
172                &message.instructions[0],
173                &AccountKeys::new(&message.account_keys, None)
174            )
175            .unwrap(),
176            ParsedInstructionEnum {
177                instruction_type: "freezeLookupTable".to_string(),
178                info: json!({
179                    "lookupTableAccount": lookup_table_pubkey.to_string(),
180                    "lookupTableAuthority": authority.to_string(),
181                }),
182            }
183        );
184        assert!(parse_address_lookup_table(
185            &message.instructions[0],
186            &AccountKeys::new(&message.account_keys[0..1], None)
187        )
188        .is_err());
189        let keys = message.account_keys.clone();
190        message.instructions[0].accounts.pop();
191        assert!(parse_address_lookup_table(
192            &message.instructions[0],
193            &AccountKeys::new(&keys, None)
194        )
195        .is_err());
196    }
197
198    #[test]
199    fn test_parse_extend_lookup_table_ix() {
200        let lookup_table_pubkey = Pubkey::new_unique();
201        let authority = Pubkey::new_unique();
202        let from_pubkey = Pubkey::new_unique();
203        let no_addresses = vec![];
204        let address0 = Pubkey::new_unique();
205        let address1 = Pubkey::new_unique();
206        let some_addresses = vec![address0, address1];
207
208        // No payer, no addresses
209        let instruction =
210            instruction::extend_lookup_table(lookup_table_pubkey, authority, None, no_addresses);
211        let mut message = Message::new(&[instruction], None);
212        assert_eq!(
213            parse_address_lookup_table(
214                &message.instructions[0],
215                &AccountKeys::new(&message.account_keys, None)
216            )
217            .unwrap(),
218            ParsedInstructionEnum {
219                instruction_type: "extendLookupTable".to_string(),
220                info: json!({
221                    "lookupTableAccount": lookup_table_pubkey.to_string(),
222                    "lookupTableAuthority": authority.to_string(),
223                    "newAddresses": [],
224                }),
225            }
226        );
227        assert!(parse_address_lookup_table(
228            &message.instructions[0],
229            &AccountKeys::new(&message.account_keys[0..1], None)
230        )
231        .is_err());
232        let keys = message.account_keys.clone();
233        message.instructions[0].accounts.pop();
234        assert!(parse_address_lookup_table(
235            &message.instructions[0],
236            &AccountKeys::new(&keys, None)
237        )
238        .is_err());
239
240        // Some payer, some addresses
241        let instruction = instruction::extend_lookup_table(
242            lookup_table_pubkey,
243            authority,
244            Some(from_pubkey),
245            some_addresses,
246        );
247        let mut message = Message::new(&[instruction], None);
248        assert_eq!(
249            parse_address_lookup_table(
250                &message.instructions[0],
251                &AccountKeys::new(&message.account_keys, None)
252            )
253            .unwrap(),
254            ParsedInstructionEnum {
255                instruction_type: "extendLookupTable".to_string(),
256                info: json!({
257                    "lookupTableAccount": lookup_table_pubkey.to_string(),
258                    "lookupTableAuthority": authority.to_string(),
259                    "payerAccount": from_pubkey.to_string(),
260                    "systemProgram": system_program::id().to_string(),
261                    "newAddresses": [
262                        address0.to_string(),
263                        address1.to_string(),
264                    ],
265                }),
266            }
267        );
268        assert!(parse_address_lookup_table(
269            &message.instructions[0],
270            &AccountKeys::new(&message.account_keys[0..1], None)
271        )
272        .is_err());
273        let keys = message.account_keys.clone();
274        message.instructions[0].accounts.pop();
275        message.instructions[0].accounts.pop();
276        message.instructions[0].accounts.pop();
277        assert!(parse_address_lookup_table(
278            &message.instructions[0],
279            &AccountKeys::new(&keys, None)
280        )
281        .is_err());
282    }
283
284    #[test]
285    fn test_parse_deactivate_lookup_table_ix() {
286        let lookup_table_pubkey = Pubkey::new_unique();
287        let authority = Pubkey::new_unique();
288
289        let instruction = instruction::deactivate_lookup_table(lookup_table_pubkey, authority);
290        let mut message = Message::new(&[instruction], None);
291        assert_eq!(
292            parse_address_lookup_table(
293                &message.instructions[0],
294                &AccountKeys::new(&message.account_keys, None)
295            )
296            .unwrap(),
297            ParsedInstructionEnum {
298                instruction_type: "deactivateLookupTable".to_string(),
299                info: json!({
300                    "lookupTableAccount": lookup_table_pubkey.to_string(),
301                    "lookupTableAuthority": authority.to_string(),
302                }),
303            }
304        );
305        assert!(parse_address_lookup_table(
306            &message.instructions[0],
307            &AccountKeys::new(&message.account_keys[0..1], None)
308        )
309        .is_err());
310        let keys = message.account_keys.clone();
311        message.instructions[0].accounts.pop();
312        assert!(parse_address_lookup_table(
313            &message.instructions[0],
314            &AccountKeys::new(&keys, None)
315        )
316        .is_err());
317    }
318
319    #[test]
320    fn test_parse_close_lookup_table_ix() {
321        let lookup_table_pubkey = Pubkey::new_unique();
322        let authority = Pubkey::new_unique();
323        let recipient = Pubkey::new_unique();
324
325        let instruction =
326            instruction::close_lookup_table(lookup_table_pubkey, authority, recipient);
327        let mut message = Message::new(&[instruction], None);
328        assert_eq!(
329            parse_address_lookup_table(
330                &message.instructions[0],
331                &AccountKeys::new(&message.account_keys, None)
332            )
333            .unwrap(),
334            ParsedInstructionEnum {
335                instruction_type: "closeLookupTable".to_string(),
336                info: json!({
337                    "lookupTableAccount": lookup_table_pubkey.to_string(),
338                    "lookupTableAuthority": authority.to_string(),
339                    "recipient": recipient.to_string(),
340                }),
341            }
342        );
343        assert!(parse_address_lookup_table(
344            &message.instructions[0],
345            &AccountKeys::new(&message.account_keys[0..2], None)
346        )
347        .is_err());
348        let keys = message.account_keys.clone();
349        message.instructions[0].accounts.pop();
350        assert!(parse_address_lookup_table(
351            &message.instructions[0],
352            &AccountKeys::new(&keys, None)
353        )
354        .is_err());
355    }
356}