1. Concepts

Instructions

InstructionMetadata, stack heights and absolute paths, nested instructions and CPIs, event CPI instructions, and decoding log events with decode_log_events.

On this page

A Solana transaction runs top-level instructions, and each of those can call other programs through cross-program invocation (CPI). Carbon decodes all of them, nested ones included, and tells your processor where each instruction sits. This page explains that position data, the nested instruction tree, and the two ways programs emit events.

Instruction metadata#

Every instruction processor receives an InstructionMetadata:

Rust
pub struct InstructionMetadata {
    pub transaction_metadata: Arc<TransactionMetadata>,
    pub stack_height: u32,
    pub index: u32,
    pub absolute_path: Vec<u8>,
}
FieldMeaning
transaction_metadataThe transaction this instruction belongs to: signature, slot, fee payer, message, execution meta (logs, balances, inner instructions), block time. Shared by every instruction of the transaction. See Transactions.
stack_height1 for a top-level instruction, 2 for an instruction it called, and so on up to 5 (MAX_INSTRUCTION_STACK_DEPTH).
indexThe index of the top-level instruction this one belongs to. An inner instruction has the same index as its top-level ancestor.
absolute_pathThe position at each level, starting with the top-level index. [3] is top-level instruction 3; [3, 0, 2] is the third instruction called by the first instruction called by top-level instruction 3.

absolute_path.len() equals stack_height, and absolute_path[0] equals index. The pair (transaction signature, absolute_path) identifies an instruction uniquely, which is how DeduplicationFilter recognizes repeats.

A Jupiter route that swaps through Meteora DLMM looks like this (simplified):

Instructions nested by stack height A Jupiter route at absolute_path [2] has stack_height 1. It calls Token: transfer at [2, 0] and Meteora DLMM: swap at [2, 3], both stack_height 2, followed by more inner instructions. The Meteora DLMM swap calls Token: transferChecked at [2, 3, 0] and [2, 3, 1] and emits its event CPI at [2, 3, 2], all stack_height 3. stack_height 1 stack_height 2 stack_height 3 Jupiter route absolute_path [2] Token: transfer absolute_path [2, 0] Meteora DLMM: swap absolute_path [2, 3] Token: transferChecked absolute_path [2, 3, 0] Token: transferChecked absolute_path [2, 3, 1] Meteora DLMM: event CPI absolute_path [2, 3, 2] more inner instructions Instructions nested by stack height A Jupiter route at absolute_path [2] has stack_height 1. It calls Token: transfer at [2, 0] and Meteora DLMM: swap at [2, 3], both stack_height 2, followed by more inner instructions. The Meteora DLMM swap calls Token: transferChecked at [2, 3, 0] and [2, 3, 1] and emits its event CPI at [2, 3, 2], all stack_height 3. Jupiter route [2] · stack_height 1 Token: transfer [2, 0] · stack_height 2 Meteora DLMM: swap [2, 3] · stack_height 2 Token: transferChecked [2, 3, 0] · stack_height 3 Token: transferChecked [2, 3, 1] · stack_height 3 Meteora DLMM: event CPI [2, 3, 2] · stack_height 3 more inner instructions

How Carbon builds the tree#

Solana records inner instructions in the transaction's metadata as one flat group per top-level instruction, each with its stack height. For every transaction, Carbon:

  1. Walks the top-level instructions in order and, after each one, its group of inner instructions.
  2. Derives each inner instruction's absolute_path from the sequence of stack heights.
  3. Resolves account keys, including those loaded from address lookup tables, and builds a solana_instruction::Instruction for each.
  4. Nests the list into NestedInstructions, a tree of NestedInstruction nodes.

An inner instruction without a stack height, or with one outside 2 to 5, can't be placed in the tree. Carbon logs a warning and drops it together with the rest of its group, so transactions whose metadata lacks stack heights reach your processors without inner instructions.

Nested instructions#

Rust
pub struct NestedInstruction {
    pub metadata: InstructionMetadata,
    pub instruction: solana_instruction::Instruction,
    pub inner_instructions: NestedInstructions,
}

pub struct NestedInstructions(pub Vec<NestedInstruction>);

NestedInstructions dereferences to &[NestedInstruction], so you can index it, call .iter() and .len(), and loop over it. In an instruction processor, input.nested_instructions holds the direct children of the current instruction; each child's inner_instructions holds its own children. The children are raw instructions: decode them with any decoder by calling decode_instruction(&child.instruction).

An instruction pipe runs its processor for every instruction its decoder recognizes, at any depth. A CPI therefore reaches you twice: once as its own processor call and once as a child of its caller. Pick one view when you count things, or you'll count CPIs twice.

Event CPI instructions#

Anchor's emit_cpi! publishes an event by having the program call itself with the event as instruction data. Generated decoders decode these self-calls as a CpiEvent variant, such as MeteoraDlmmInstruction::CpiEvent { data: CpiEvent::Swap(event), .. }. Because the event is a child of the instruction that emitted it, you can pair them: look for the CpiEvent among a swap's nested_instructions to get the swap's actual output amount.

Log events#

Anchor's emit! publishes an event as a Program data: <base64> line in the transaction's log. InstructionMetadata::decode_log_events returns the events logged by one instruction:

Rust
pub fn decode_log_events<T: CarbonDeserialize>(&self) -> Vec<T>

It follows the Program … invoke [n] and Program … success lines to find the log lines that belong to this instruction's absolute_path (excluding its CPIs, and accounting for precompile instructions, which don't log), base64-decodes them, and keeps those that T::deserialize accepts. It returns an empty list if the transaction has no log messages, and it can't recover events from logs the runtime truncated.

T must implement CarbonDeserialize. Generated event types don't, so wrap them. This newtype decodes Meteora DLMM's SwapEvent from log lines; its deserialize calls the generated decode, which checks the discriminator:

Rust
use {
    borsh::BorshDeserialize,
    carbon_core::deserialize::CarbonDeserialize,
    carbon_meteora_dlmm_decoder::events::swap::SwapEvent,
};

/// A `SwapEvent` emitted with Anchor's `emit!` (a `Program data:` log line).
#[derive(Debug, BorshDeserialize)]
pub struct LoggedSwap(pub SwapEvent);

impl CarbonDeserialize for LoggedSwap {
    const DISCRIMINATOR: &'static [u8] = &[81, 108, 227, 190, 205, 208, 10, 196];

    fn deserialize(data: &[u8]) -> Option<Self> {
        SwapEvent::decode(data).map(LoggedSwap)
    }
}

Then, in an instruction processor:

Rust
for LoggedSwap(event) in input.metadata.decode_log_events::<LoggedSwap>() {
    println!("logged swap: amount_out={}", event.amount_out);
}

Meteora DLMM itself emits through event CPI, so in practice you use this pattern for programs that log their events, such as Raydium CLMM.

Example#

This program crawls Meteora DLMM transactions from the public RPC endpoint. For each swap it prints the swap's position, then finds the swap's event CPI among its children and prints the output amount and fee from it.

Terminal
cargo add carbon-core@2.0.0 carbon-meteora-dlmm-decoder@2.0.0 carbon-rpc-transaction-crawler-datasource@2.0.0
cargo add tokio --features full
cargo add env_logger
src/main.rs
use {
    carbon_core::{
        error::CarbonResult,
        instruction::{InstructionDecoder, InstructionProcessorInputType},
        pipeline::Pipeline,
        processor::Processor,
    },
    carbon_meteora_dlmm_decoder::{
        instructions::{cpi_event::CpiEvent, MeteoraDlmmInstruction},
        MeteoraDlmmDecoder, PROGRAM_ID,
    },
    carbon_rpc_transaction_crawler_datasource::{ConnectionConfig, Filters, RpcTransactionCrawler},
};

struct SwapTree;

impl Processor<InstructionProcessorInputType<'_, MeteoraDlmmInstruction>> for SwapTree {
    async fn process(
        &mut self,
        input: &InstructionProcessorInputType<'_, MeteoraDlmmInstruction>,
    ) -> CarbonResult<()> {
        let MeteoraDlmmInstruction::Swap { data, .. } = input.decoded_instruction else {
            return Ok(());
        };
        let meta = input.metadata;
        println!(
            "swap {} path={:?} stack_height={} amount_in={}",
            meta.transaction_metadata.signature,
            meta.absolute_path,
            meta.stack_height,
            data.amount_in
        );

        // Children of this instruction: token transfers and the program's event CPI.
        for child in input.nested_instructions.iter() {
            if let Some(MeteoraDlmmInstruction::CpiEvent {
                data: CpiEvent::Swap(event),
                ..
            }) = MeteoraDlmmDecoder.decode_instruction(&child.instruction)
            {
                println!(
                    "  event path={:?} amount_out={} fee={}",
                    child.metadata.absolute_path, event.amount_out, event.fee
                );
            }
        }
        Ok(())
    }
}

#[tokio::main]
async fn main() -> CarbonResult<()> {
    env_logger::init();

    let crawler = RpcTransactionCrawler::new(
        "https://api.mainnet-beta.solana.com".to_string(),
        PROGRAM_ID,
        ConnectionConfig::default(),
        Filters::new(None, None, None),
        None,
    );

    Pipeline::builder()
        .datasource(crawler)
        .instruction(MeteoraDlmmDecoder, SwapTree)
        .build()?
        .run()
        .await
}
Terminal
cargo run
Output
swap 4qSjUGPcKNNcEUxcK2uaTUYKoTrUBYCYEFLbZt8yqiWxrH7Z2E8nXCj4zMQjJ1TxNYztjWC83qg2WXKhSjUTc1gw path=[3, 0, 2] stack_height=3 amount_in=1679907
  event path=[3, 0, 2, 2] amount_out=19337053 fee=233
swap 24cQfJCNzD67i83gUk4jV8Z6sNz9TrPXZV8kdZjKK3sNeUHLXAxddb3JmKoETAEgsFrpJjWzyqg5qr2fuj8nzZXc path=[0, 0] stack_height=2 amount_in=4254885455
  event path=[0, 0, 2] amount_out=11300348 fee=65622
swap 4pr7pw96PGJSiXYcGT3Vman8v1rgbjEn8XASQmkdURRayQHsJZut88G9mwyzYhWH1XksEtBAdALj5dD13VYoRgM5 path=[2, 3] stack_height=2 amount_in=854429039
  event path=[2, 3, 2] amount_out=318436120 fee=349280
…

Most DLMM swaps arrive through an aggregator, so their paths have two or more levels. The public endpoint is rate limited; output can pause for a minute or more, and you may see 429 warnings in the log. Press Ctrl+C to stop.

Next steps#

Your endpoints

Code samples use these instead of the placeholders, so you can copy and run them as they are. They’re stored only in this browser and never sent anywhere.