ellipsis_transaction_utils/
lib.rs

1use itertools::Itertools;
2use serde::{Deserialize, Serialize};
3use solana_sdk::{
4    clock::UnixTimestamp, instruction::CompiledInstruction, message::VersionedMessage,
5};
6use solana_transaction_status::{
7    option_serializer::OptionSerializer, EncodedConfirmedTransactionWithStatusMeta,
8    EncodedTransaction, EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiInstruction,
9    UiMessage, UiParsedInstruction, VersionedTransactionWithStatusMeta,
10};
11
12#[derive(Clone, Debug, Deserialize, Serialize)]
13pub struct ParsedTransaction {
14    pub slot: u64,
15    pub block_time: Option<UnixTimestamp>,
16    pub instructions: Vec<ParsedInstruction>,
17    pub inner_instructions: Vec<Vec<ParsedInnerInstruction>>,
18    pub logs: Vec<String>,
19    pub is_err: bool,
20    pub signature: String,
21    pub fee_payer: String,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct ParsedInstruction {
26    pub program_id: String,
27    pub accounts: Vec<String>,
28    pub data: Vec<u8>,
29    pub stack_height: Option<u32>,
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize)]
33pub struct ParsedInnerInstruction {
34    pub parent_index: usize,
35    pub instruction: ParsedInstruction,
36}
37
38pub fn parse_ui_compiled_instruction(
39    c: &UiCompiledInstruction,
40    account_keys: &[String],
41) -> ParsedInstruction {
42    ParsedInstruction {
43        program_id: account_keys[c.program_id_index as usize].clone(),
44        accounts: c
45            .accounts
46            .iter()
47            .map(|i| account_keys[*i as usize].clone())
48            .collect(),
49        data: bs58::decode(c.data.clone()).into_vec().unwrap(),
50        stack_height: c.stack_height,
51    }
52}
53
54pub fn parse_ui_instruction(
55    ui_instruction: &UiInstruction,
56    account_keys: &[String],
57) -> ParsedInstruction {
58    match ui_instruction {
59        UiInstruction::Compiled(c) => parse_ui_compiled_instruction(c, account_keys),
60        UiInstruction::Parsed(p) => match p {
61            UiParsedInstruction::PartiallyDecoded(pd) => ParsedInstruction {
62                program_id: pd.program_id.clone(),
63                accounts: pd.accounts.clone(),
64                data: bs58::decode(&pd.data.clone()).into_vec().unwrap(),
65                stack_height: pd.stack_height,
66            },
67            _ => panic!("Unsupported instruction encoding"),
68        },
69    }
70}
71
72pub fn parse_compiled_instruction(
73    instruction: &CompiledInstruction,
74    account_keys: &[String],
75) -> ParsedInstruction {
76    ParsedInstruction {
77        program_id: account_keys[instruction.program_id_index as usize].clone(),
78        accounts: instruction
79            .accounts
80            .iter()
81            .map(|i| account_keys[*i as usize].clone())
82            .collect(),
83        data: instruction.data.clone(),
84        stack_height: Some(1),
85    }
86}
87
88pub fn parse_ui_message(
89    message: UiMessage,
90    loaded_addresses: &[String],
91) -> (Vec<String>, Vec<ParsedInstruction>) {
92    match message {
93        UiMessage::Parsed(p) => {
94            let mut keys = p
95                .account_keys
96                .iter()
97                .map(|k| k.pubkey.to_string())
98                .collect::<Vec<String>>();
99            keys.extend_from_slice(loaded_addresses);
100            (
101                keys.clone(),
102                p.instructions
103                    .iter()
104                    .map(|i| parse_ui_instruction(i, &keys))
105                    .collect::<Vec<ParsedInstruction>>(),
106            )
107        }
108        UiMessage::Raw(r) => {
109            let mut keys = r.account_keys.clone();
110            keys.extend_from_slice(loaded_addresses);
111            (
112                keys.clone(),
113                r.instructions
114                    .iter()
115                    .map(|i| parse_ui_compiled_instruction(i, &keys))
116                    .collect::<Vec<ParsedInstruction>>(),
117            )
118        }
119    }
120}
121
122pub fn parse_versioned_message(
123    message: VersionedMessage,
124    loaded_addresses: &[String],
125) -> (Vec<String>, Vec<ParsedInstruction>) {
126    let mut keys = message
127        .static_account_keys()
128        .into_iter()
129        .map(|pk| pk.to_string())
130        .collect_vec();
131    keys.extend_from_slice(loaded_addresses);
132    let instructions = message
133        .instructions()
134        .iter()
135        .map(|i| parse_compiled_instruction(i, &keys))
136        .collect_vec();
137    (keys, instructions)
138}
139
140pub fn parse_transaction(tx: EncodedConfirmedTransactionWithStatusMeta) -> ParsedTransaction {
141    let slot = tx.slot;
142    let block_time = tx.block_time;
143
144    let tx_meta = tx.transaction.meta.unwrap();
145    let loaded_addresses = match tx_meta.loaded_addresses {
146        OptionSerializer::Some(l) => [l.writable, l.readonly].concat(),
147        _ => vec![],
148    };
149
150    let (keys, instructions, signature) = match tx.transaction.transaction {
151        EncodedTransaction::Json(t) => {
152            let (keys, instructions) = parse_ui_message(t.message, &loaded_addresses);
153            let signature = t.signatures[0].to_string();
154            (keys, instructions, signature)
155        }
156        _ => {
157            let versioned_tx = tx
158                .transaction
159                .transaction
160                .decode()
161                .expect("Failed to decode transaction");
162            let (keys, instructions) =
163                parse_versioned_message(versioned_tx.message, &loaded_addresses);
164            (keys, instructions, versioned_tx.signatures[0].to_string())
165        }
166    };
167
168    let is_err = tx_meta.err.is_some();
169    let logs = match tx_meta.log_messages {
170        OptionSerializer::Some(l) => l,
171        _ => vec![],
172    };
173    let inner_instructions = match tx_meta.inner_instructions {
174        OptionSerializer::Some(inner) => inner
175            .iter()
176            .map(|ii| {
177                ii.instructions
178                    .iter()
179                    .map(|i| ParsedInnerInstruction {
180                        parent_index: ii.index as usize,
181                        instruction: parse_ui_instruction(i, &keys),
182                    })
183                    .collect::<Vec<ParsedInnerInstruction>>()
184            })
185            .collect::<Vec<Vec<ParsedInnerInstruction>>>(),
186        _ => vec![],
187    };
188    ParsedTransaction {
189        slot,
190        block_time,
191        instructions,
192        inner_instructions,
193        logs,
194        is_err,
195        signature,
196        fee_payer: keys[0].clone(),
197    }
198}
199
200pub fn parse_versioned_transaction(
201    slot: u64,
202    block_time: Option<i64>,
203    tx: VersionedTransactionWithStatusMeta,
204) -> Option<ParsedTransaction> {
205    let tx_meta = tx.meta;
206    let is_err = tx_meta.status.is_err();
207    if is_err {
208        return None;
209    }
210    let loaded_addresses = [
211        tx_meta.loaded_addresses.writable,
212        tx_meta.loaded_addresses.readonly,
213    ]
214    .concat()
215    .iter()
216    .map(|x| x.to_string())
217    .collect::<Vec<String>>();
218
219    let (keys, instructions) =
220        { parse_versioned_message(tx.transaction.message, loaded_addresses.as_slice()) };
221
222    let logs = tx_meta.log_messages.unwrap_or_default();
223    let inner_instructions = tx_meta
224        .inner_instructions
225        .unwrap_or_default()
226        .iter()
227        .map(|ii| {
228            ii.instructions
229                .iter()
230                .map(|i| ParsedInnerInstruction {
231                    parent_index: ii.index as usize,
232                    instruction: parse_compiled_instruction(&i.instruction, &keys),
233                })
234                .collect::<Vec<ParsedInnerInstruction>>()
235        })
236        .collect::<Vec<Vec<ParsedInnerInstruction>>>();
237    Some(ParsedTransaction {
238        slot,
239        signature: tx.transaction.signatures[0].to_string(),
240        block_time,
241        instructions,
242        inner_instructions,
243        logs,
244        is_err,
245        fee_payer: keys[0].clone(),
246    })
247}
248
249pub fn parse_encoded_transaction_with_status_meta(
250    slot: u64,
251    block_time: Option<i64>,
252    tx: EncodedTransactionWithStatusMeta,
253) -> Option<ParsedTransaction> {
254    let tx_meta = tx.meta?;
255    let loaded_addresses = match tx_meta.loaded_addresses {
256        OptionSerializer::Some(la) => [la.writable, la.readonly].concat(),
257        _ => vec![],
258    };
259
260    let versioned_tx = tx.transaction.decode()?;
261    let (keys, instructions) =
262        { parse_versioned_message(versioned_tx.message, loaded_addresses.as_slice()) };
263
264    let is_err = tx_meta.status.is_err();
265    let logs = match tx_meta.log_messages {
266        OptionSerializer::Some(lm) => lm,
267        _ => vec![],
268    };
269    let inner_instructions = match tx_meta.inner_instructions {
270        OptionSerializer::Some(inner_instructions) => inner_instructions
271            .iter()
272            .map(|ii| {
273                ii.instructions
274                    .iter()
275                    .map(|i| ParsedInnerInstruction {
276                        parent_index: ii.index as usize,
277                        instruction: parse_ui_instruction(i, &keys),
278                    })
279                    .collect::<Vec<ParsedInnerInstruction>>()
280            })
281            .collect::<Vec<Vec<ParsedInnerInstruction>>>(),
282        _ => vec![],
283    };
284    Some(ParsedTransaction {
285        slot,
286        signature: versioned_tx.signatures[0].to_string(),
287        block_time,
288        instructions,
289        inner_instructions,
290        logs,
291        is_err,
292        fee_payer: keys[0].clone(),
293    })
294}
295
296#[derive(Debug, Clone)]
297pub struct InstructionStack {
298    pub instruction: ParsedInstruction,
299    pub stack_height: u32,
300    pub instructions_in_stack: Vec<InstructionStack>,
301    pub flatten_index: usize,
302}
303
304impl InstructionStack {
305    pub fn from_parsed_transaction(tx: &ParsedTransaction) -> Vec<InstructionStack> {
306        // First create stacks with temporary indices
307        let mut instruction_stacks = tx
308            .instructions
309            .iter()
310            .map(|instruction| InstructionStack {
311                instruction: instruction.clone(),
312                // transaction.instructions is the outermost instructions, and the stack_height is 1 (from Solana's impl)
313                stack_height: 1,
314                instructions_in_stack: vec![],
315                flatten_index: 0, // will be updated in the second pass
316            })
317            .collect::<Vec<_>>();
318
319        // build the tree structure
320        for ii in tx.inner_instructions.iter() {
321            if let Some(first_ii) = ii.first() {
322                let current_instruction_stack =
323                    &mut instruction_stacks[first_ii.parent_index as usize];
324                parse_inner_instuction_stack(
325                    current_instruction_stack,
326                    // Here we start with the outermost *inner* instructions, where the stack_height begins at 2 (cuz it's the second level of instructions)
327                    2,
328                    0,
329                    &ii,
330                    &mut 0, // dummy value
331                );
332            }
333        }
334
335        // assign the flatten index by dfs
336        let mut current_index = 0;
337        for stack in instruction_stacks.iter_mut() {
338            assign_indices_dfs(stack, &mut current_index);
339        }
340
341        instruction_stacks
342    }
343}
344
345fn assign_indices_dfs(stack: &mut InstructionStack, current_index: &mut usize) {
346    stack.flatten_index = *current_index;
347    *current_index += 1;
348
349    for inner in stack.instructions_in_stack.iter_mut() {
350        assign_indices_dfs(inner, current_index);
351    }
352}
353
354fn parse_inner_instuction_stack(
355    current_stack: &mut InstructionStack,
356    current_stack_height: u32,
357    inner_inst_cursor: usize,
358    inner_instructions: &Vec<ParsedInnerInstruction>,
359    total_instructions: &mut usize,
360) -> usize {
361    if inner_inst_cursor >= inner_instructions.len() {
362        return inner_inst_cursor;
363    }
364
365    let current_instruction = &inner_instructions[inner_inst_cursor];
366    let stack_height = current_instruction
367        .instruction
368        .stack_height
369        .expect("Inner instruction must have stack height");
370
371    if stack_height < current_stack_height {
372        return inner_inst_cursor;
373    }
374
375    if stack_height == current_stack_height {
376        current_stack.instructions_in_stack.push(InstructionStack {
377            instruction: current_instruction.instruction.clone(),
378            stack_height: current_stack_height,
379            instructions_in_stack: vec![],
380            flatten_index: 0, // dummy value
381        });
382        *total_instructions += 1;
383
384        // Process next instruction at same level
385        return parse_inner_instuction_stack(
386            current_stack,
387            current_stack_height,
388            inner_inst_cursor + 1,
389            inner_instructions,
390            total_instructions,
391        );
392    }
393
394    // case when stack_height > current
395    assert_eq!(
396        stack_height,
397        current_stack_height + 1,
398        "Stack height can only increase by 1"
399    );
400
401    // Get the most recently added instruction stack
402    let last_stack = current_stack
403        .instructions_in_stack
404        .last_mut()
405        .expect("Must have a parent instruction before going deeper");
406
407    // Process instruction at deeper level and continue with remaining instructions
408    let new_cursor = parse_inner_instuction_stack(
409        last_stack,
410        stack_height,
411        inner_inst_cursor,
412        inner_instructions,
413        total_instructions,
414    );
415
416    parse_inner_instuction_stack(
417        current_stack,
418        current_stack_height,
419        new_cursor,
420        inner_instructions,
421        total_instructions,
422    )
423}