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