carbon_core/
transformers.rs

1//! Provides utility functions to transform transaction data into various
2//! representations within the `carbon-core` framework.
3//!
4//! This module includes functions for extracting transaction metadata, parsing
5//! instructions, and nesting instructions based on stack depth. It also offers
6//! transformations for Solana transaction components into suitable formats for
7//! the framework, enabling flexible processing of transaction data.
8//!
9//! ## Key Components
10//!
11//! - **Metadata Extraction**: Extracts essential transaction metadata for
12//!   processing.
13//! - **Instruction Parsing**: Parses both top-level and nested instructions
14//!   from transactions.
15//! - **Account Metadata**: Converts account data into a standardized format for
16//!   transactions.
17//!
18//! ## Notes
19//!
20//! - The module supports both legacy and v0 transactions, including handling of
21//!   loaded addresses and inner instructions.
22
23use {
24    crate::{
25        collection::InstructionDecoderCollection,
26        datasource::TransactionUpdate,
27        error::{CarbonResult, Error},
28        instruction::{DecodedInstruction, InstructionMetadata},
29        schema::ParsedInstruction,
30        transaction::TransactionMetadata,
31    },
32    solana_instruction::AccountMeta,
33    solana_program::{
34        instruction::CompiledInstruction,
35        message::{
36            v0::{LoadedAddresses, LoadedMessage},
37            VersionedMessage,
38        },
39    },
40    solana_pubkey::Pubkey,
41    solana_sdk::{
42        reserved_account_keys::ReservedAccountKeys,
43        transaction_context::TransactionReturnData, // TODO: replace with solana_transaction_context after release of 2.2.0
44    },
45    solana_transaction_status::{
46        option_serializer::OptionSerializer, InnerInstruction, InnerInstructions, Reward,
47        TransactionStatusMeta, TransactionTokenBalance, UiInstruction, UiLoadedAddresses,
48        UiTransactionStatusMeta,
49    },
50    std::{collections::HashSet, str::FromStr},
51};
52
53/// Extracts instructions with metadata from a transaction update.
54///
55/// This function parses both top-level and inner instructions, associating them
56/// with metadata such as stack height and account information. It provides a
57/// detailed breakdown of each instruction, useful for further processing.
58///
59/// # Parameters
60///
61/// - `transaction_metadata`: Metadata about the transaction from which
62///   instructions are extracted.
63/// - `transaction_update`: The `TransactionUpdate` containing the transaction's
64///   data and message.
65///
66/// # Returns
67///
68/// A `CarbonResult<Vec<(InstructionMetadata,
69/// solana_instruction::Instruction)>>` containing instructions along with
70/// their associated metadata.
71///
72/// # Errors
73///
74/// Returns an error if any account metadata required for instruction processing
75/// is missing.
76pub fn extract_instructions_with_metadata(
77    transaction_metadata: &TransactionMetadata,
78    transaction_update: &TransactionUpdate,
79) -> CarbonResult<Vec<(InstructionMetadata, solana_instruction::Instruction)>> {
80    log::trace!(
81        "extract_instructions_with_metadata(transaction_metadata: {:?}, transaction_update: {:?})",
82        transaction_metadata,
83        transaction_update
84    );
85    let message = transaction_update.transaction.message.clone();
86    let meta = transaction_update.meta.clone();
87
88    let mut instructions_with_metadata =
89        Vec::<(InstructionMetadata, solana_instruction::Instruction)>::new();
90
91    match message {
92        VersionedMessage::Legacy(legacy) => {
93            for (i, compiled_instruction) in legacy.instructions.iter().enumerate() {
94                let program_id = *legacy
95                    .account_keys
96                    .get(compiled_instruction.program_id_index as usize)
97                    .unwrap_or(&Pubkey::default());
98
99                let accounts: Vec<_> = compiled_instruction
100                    .accounts
101                    .iter()
102                    .filter_map(|account_index| {
103                        let account_pubkey = legacy.account_keys.get(*account_index as usize)?;
104                        Some(AccountMeta {
105                            pubkey: *account_pubkey,
106                            is_writable: legacy.is_maybe_writable(*account_index as usize, None),
107                            is_signer: legacy.is_signer(*account_index as usize),
108                        })
109                    })
110                    .collect();
111
112                instructions_with_metadata.push((
113                    InstructionMetadata {
114                        transaction_metadata: transaction_metadata.clone(),
115                        stack_height: 1,
116                        index: i as u32 + 1,
117                    },
118                    solana_instruction::Instruction {
119                        program_id,
120                        accounts,
121                        data: compiled_instruction.data.clone(),
122                    },
123                ));
124
125                if let Some(inner_instructions) = &meta.inner_instructions {
126                    for inner_instructions_per_tx in inner_instructions {
127                        if inner_instructions_per_tx.index == i as u8 {
128                            for (inner_ix_idx, inner_instruction) in
129                                inner_instructions_per_tx.instructions.iter().enumerate()
130                            {
131                                let program_id = *legacy
132                                    .account_keys
133                                    .get(inner_instruction.instruction.program_id_index as usize)
134                                    .unwrap_or(&Pubkey::default());
135
136                                let accounts: Vec<_> = inner_instruction
137                                    .instruction
138                                    .accounts
139                                    .iter()
140                                    .filter_map(|account_index| {
141                                        let account_pubkey =
142                                            legacy.account_keys.get(*account_index as usize)?;
143
144                                        Some(AccountMeta {
145                                            pubkey: *account_pubkey,
146                                            is_writable: legacy
147                                                .is_maybe_writable(*account_index as usize, None),
148                                            is_signer: legacy.is_signer(*account_index as usize),
149                                        })
150                                    })
151                                    .collect();
152
153                                instructions_with_metadata.push((
154                                    InstructionMetadata {
155                                        transaction_metadata: transaction_metadata.clone(),
156                                        stack_height: inner_instruction.stack_height.unwrap_or(1),
157                                        index: inner_ix_idx as u32 + 1,
158                                    },
159                                    solana_instruction::Instruction {
160                                        program_id,
161                                        accounts,
162                                        data: inner_instruction.instruction.data.clone(),
163                                    },
164                                ));
165                            }
166                        }
167                    }
168                }
169            }
170        }
171        VersionedMessage::V0(v0) => {
172            let loaded_addresses = LoadedAddresses {
173                writable: meta.loaded_addresses.writable.to_vec(),
174                readonly: meta.loaded_addresses.readonly.to_vec(),
175            };
176
177            let loaded_message = LoadedMessage::new(
178                v0.clone(),
179                loaded_addresses,
180                &ReservedAccountKeys::empty_key_set(),
181            );
182
183            for (i, compiled_instruction) in v0.instructions.iter().enumerate() {
184                let program_id = *loaded_message
185                    .account_keys()
186                    .get(compiled_instruction.program_id_index as usize)
187                    .unwrap_or(&Pubkey::default());
188
189                let accounts: Vec<AccountMeta> = compiled_instruction
190                    .accounts
191                    .iter()
192                    .map(|account_index| {
193                        let account_pubkey =
194                            loaded_message.account_keys().get(*account_index as usize);
195
196                        AccountMeta {
197                            pubkey: account_pubkey.copied().unwrap_or_default(),
198                            is_writable: loaded_message.is_writable(*account_index as usize),
199                            is_signer: loaded_message.is_signer(*account_index as usize),
200                        }
201                    })
202                    .collect();
203
204                instructions_with_metadata.push((
205                    InstructionMetadata {
206                        transaction_metadata: transaction_metadata.clone(),
207                        stack_height: 1,
208                        index: i as u32 + 1,
209                    },
210                    solana_instruction::Instruction {
211                        program_id,
212                        accounts,
213                        data: compiled_instruction.data.clone(),
214                    },
215                ));
216
217                if let Some(inner_instructions) = &meta.inner_instructions {
218                    for inner_instructions_per_tx in inner_instructions {
219                        if inner_instructions_per_tx.index == i as u8 {
220                            for (inner_ix_idx, inner_instruction) in
221                                inner_instructions_per_tx.instructions.iter().enumerate()
222                            {
223                                let program_id = *loaded_message
224                                    .account_keys()
225                                    .get(inner_instruction.instruction.program_id_index as usize)
226                                    .unwrap_or(&Pubkey::default());
227
228                                let accounts: Vec<AccountMeta> = inner_instruction
229                                    .instruction
230                                    .accounts
231                                    .iter()
232                                    .map(|account_index| {
233                                        let account_pubkey = loaded_message
234                                            .account_keys()
235                                            .get(*account_index as usize)
236                                            .copied()
237                                            .unwrap_or_default();
238
239                                        AccountMeta {
240                                            pubkey: account_pubkey,
241                                            is_writable: loaded_message
242                                                .is_writable(*account_index as usize),
243                                            is_signer: loaded_message
244                                                .is_signer(*account_index as usize),
245                                        }
246                                    })
247                                    .collect();
248
249                                instructions_with_metadata.push((
250                                    InstructionMetadata {
251                                        transaction_metadata: transaction_metadata.clone(),
252                                        stack_height: inner_instruction.stack_height.unwrap_or(1),
253                                        index: inner_ix_idx as u32 + 1,
254                                    },
255                                    solana_instruction::Instruction {
256                                        program_id,
257                                        accounts,
258                                        data: inner_instruction.instruction.data.clone(),
259                                    },
260                                ));
261                            }
262                        }
263                    }
264                }
265            }
266        }
267    }
268
269    Ok(instructions_with_metadata)
270}
271
272/// Extracts account metadata from a compiled instruction and transaction
273/// message.
274///
275/// This function converts each account index within the instruction into an
276/// `AccountMeta` struct, providing details on account keys, signer status, and
277/// write permissions.
278///
279/// # Parameters
280///
281/// - `compiled_instruction`: The compiled instruction to extract accounts from.
282/// - `message`: The transaction message containing the account keys.
283///
284/// # Returns
285///
286/// A `CarbonResult<&[AccountMeta]>` containing
287/// metadata for each account involved in the instruction.
288///
289/// # Errors
290///
291/// Returns an error if any referenced account key is missing from the
292/// transaction.
293pub fn extract_account_metas(
294    compiled_instruction: &CompiledInstruction,
295    message: &VersionedMessage,
296) -> CarbonResult<Vec<AccountMeta>> {
297    log::trace!(
298        "extract_account_metas(compiled_instruction: {:?}, message: {:?})",
299        compiled_instruction,
300        message
301    );
302    let mut accounts = Vec::<AccountMeta>::with_capacity(compiled_instruction.accounts.len());
303
304    for account_index in compiled_instruction.accounts.iter() {
305        accounts.push(AccountMeta {
306            pubkey: *message
307                .static_account_keys()
308                .get(*account_index as usize)
309                .ok_or(Error::MissingAccountInTransaction)?,
310            is_signer: message.is_signer(*account_index as usize),
311            is_writable: message.is_maybe_writable(
312                *account_index as usize,
313                Some(
314                    &message
315                        .static_account_keys()
316                        .iter()
317                        .copied()
318                        .collect::<HashSet<_>>(),
319                ),
320            ),
321        });
322    }
323
324    Ok(accounts)
325}
326
327/// Unnests parsed instructions, producing an array of `(InstructionMetadata,
328/// DecodedInstruction<T>)` tuple
329///
330/// This function takes a vector of `ParsedInstruction` and unnests them into a
331/// vector of `(InstructionMetadata, DecodedInstruction<T>)` tuples.
332/// It recursively processes nested instructions, increasing the stack height
333/// for each level of nesting.
334///
335/// # Parameters
336///
337/// - `transaction_metadata`: The metadata of the transaction containing the
338///   instructions.
339/// - `instructions`: The vector of `ParsedInstruction` to be unnested.
340/// - `stack_height`: The current stack height.
341///
342/// # Returns
343///
344/// A vector of `(InstructionMetadata, DecodedInstruction<T>)` tuples
345/// representing the unnested instructions.
346pub fn unnest_parsed_instructions<T: InstructionDecoderCollection>(
347    transaction_metadata: TransactionMetadata,
348    instructions: Vec<ParsedInstruction<T>>,
349    stack_height: u32,
350) -> Vec<(InstructionMetadata, DecodedInstruction<T>)> {
351    log::trace!(
352        "unnest_parsed_instructions(instructions: {:?})",
353        instructions
354    );
355
356    let mut result = Vec::new();
357
358    for (ix_idx, parsed_instruction) in instructions.into_iter().enumerate() {
359        result.push((
360            InstructionMetadata {
361                transaction_metadata: transaction_metadata.clone(),
362                stack_height,
363                index: ix_idx as u32 + 1,
364            },
365            parsed_instruction.instruction,
366        ));
367        result.extend(unnest_parsed_instructions(
368            transaction_metadata.clone(),
369            parsed_instruction.inner_instructions,
370            stack_height + 1,
371        ));
372    }
373
374    result
375}
376
377/// Converts UI transaction metadata into `TransactionStatusMeta`.
378///
379/// This function transforms the user interface format of transaction metadata
380/// into a more comprehensive `TransactionStatusMeta` structure suitable for
381/// backend processing.
382///
383/// # Parameters
384///
385/// - `meta_original`: The original UI format of transaction status metadata.
386///
387/// # Returns
388///
389/// A `CarbonResult<TransactionStatusMeta>` representing the full transaction
390/// status with nested instructions, token balances, and rewards.
391///
392/// # Notes
393///
394/// This function handles various metadata fields, including inner instructions,
395/// token balances, and rewards, providing a complete view of the transaction's
396/// effects.
397pub fn transaction_metadata_from_original_meta(
398    meta_original: UiTransactionStatusMeta,
399) -> CarbonResult<TransactionStatusMeta> {
400    log::trace!(
401        "transaction_metadata_from_original_meta(meta_original: {:?})",
402        meta_original
403    );
404    Ok(TransactionStatusMeta {
405        status: meta_original.status,
406        fee: meta_original.fee,
407        pre_balances: meta_original.pre_balances,
408        post_balances: meta_original.post_balances,
409        inner_instructions: Some(
410            meta_original
411                .inner_instructions
412                .unwrap_or_else(std::vec::Vec::new)
413                .iter()
414                .map(|inner_instruction_group| InnerInstructions {
415                    index: inner_instruction_group.index,
416                    instructions: inner_instruction_group
417                        .instructions
418                        .iter()
419                        .map(|ui_instruction| match ui_instruction {
420                            UiInstruction::Compiled(compiled_ui_instruction) => {
421                                let decoded_data =
422                                    bs58::decode(compiled_ui_instruction.data.clone())
423                                        .into_vec()
424                                        .unwrap_or_else(|_| vec![]);
425                                InnerInstruction {
426                                    instruction: CompiledInstruction {
427                                        program_id_index: compiled_ui_instruction.program_id_index,
428                                        accounts: compiled_ui_instruction.accounts.clone(),
429                                        data: decoded_data,
430                                    },
431                                    stack_height: compiled_ui_instruction.stack_height,
432                                }
433                            }
434                            _ => {
435                                log::error!("Unsupported instruction type encountered");
436                                InnerInstruction {
437                                    instruction: CompiledInstruction {
438                                        program_id_index: 0,
439                                        accounts: vec![],
440                                        data: vec![],
441                                    },
442                                    stack_height: None,
443                                }
444                            }
445                        })
446                        .collect::<Vec<InnerInstruction>>(),
447                })
448                .collect::<Vec<InnerInstructions>>(),
449        ),
450        log_messages: Some(
451            meta_original
452                .log_messages
453                .unwrap_or_else(std::vec::Vec::new),
454        ),
455        pre_token_balances: Some(
456            meta_original
457                .pre_token_balances
458                .unwrap_or_else(std::vec::Vec::new)
459                .iter()
460                .filter_map(|transaction_token_balance| {
461                    if let (OptionSerializer::Some(owner), OptionSerializer::Some(program_id)) = (
462                        transaction_token_balance.owner.as_ref(),
463                        transaction_token_balance.program_id.as_ref(),
464                    ) {
465                        Some(TransactionTokenBalance {
466                            account_index: transaction_token_balance.account_index,
467                            mint: transaction_token_balance.mint.clone(),
468                            ui_token_amount: transaction_token_balance.ui_token_amount.clone(),
469                            owner: owner.to_string(),
470                            program_id: program_id.to_string(),
471                        })
472                    } else {
473                        None
474                    }
475                })
476                .collect::<Vec<TransactionTokenBalance>>(),
477        ),
478        post_token_balances: Some(
479            meta_original
480                .post_token_balances
481                .unwrap_or_else(std::vec::Vec::new)
482                .iter()
483                .filter_map(|transaction_token_balance| {
484                    if let (OptionSerializer::Some(owner), OptionSerializer::Some(program_id)) = (
485                        transaction_token_balance.owner.as_ref(),
486                        transaction_token_balance.program_id.as_ref(),
487                    ) {
488                        Some(TransactionTokenBalance {
489                            account_index: transaction_token_balance.account_index,
490                            mint: transaction_token_balance.mint.clone(),
491                            ui_token_amount: transaction_token_balance.ui_token_amount.clone(),
492                            owner: owner.to_string(),
493                            program_id: program_id.to_string(),
494                        })
495                    } else {
496                        None
497                    }
498                })
499                .collect::<Vec<TransactionTokenBalance>>(),
500        ),
501        rewards: Some(
502            meta_original
503                .rewards
504                .unwrap_or_else(std::vec::Vec::new)
505                .iter()
506                .map(|rewards| Reward {
507                    pubkey: rewards.pubkey.clone(),
508                    lamports: rewards.lamports,
509                    post_balance: rewards.post_balance,
510                    reward_type: rewards.reward_type,
511                    commission: rewards.commission,
512                })
513                .collect::<Vec<Reward>>(),
514        ),
515        loaded_addresses: {
516            let loaded = meta_original
517                .loaded_addresses
518                .unwrap_or_else(|| UiLoadedAddresses {
519                    writable: vec![],
520                    readonly: vec![],
521                });
522            LoadedAddresses {
523                writable: loaded
524                    .writable
525                    .iter()
526                    .map(|w| Pubkey::from_str(w).unwrap_or_default())
527                    .collect::<Vec<Pubkey>>(),
528                readonly: loaded
529                    .readonly
530                    .iter()
531                    .map(|r| Pubkey::from_str(r).unwrap_or_default())
532                    .collect::<Vec<Pubkey>>(),
533            }
534        },
535        return_data: meta_original
536            .return_data
537            .map(|return_data| TransactionReturnData {
538                program_id: return_data.program_id.parse().unwrap_or_default(),
539                data: return_data.data.0.as_bytes().to_vec(),
540            }),
541        compute_units_consumed: meta_original
542            .compute_units_consumed
543            .map(|compute_unit_consumed| compute_unit_consumed)
544            .or(None),
545    })
546}