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:
pub struct InstructionMetadata {
pub transaction_metadata: Arc<TransactionMetadata>,
pub stack_height: u32,
pub index: u32,
pub absolute_path: Vec<u8>,
}| Field | Meaning |
|---|---|
transaction_ | The 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_ | 1 for a top-level instruction, 2 for an instruction it called, and so on up to 5 (MAX_). |
index | The index of the top-level instruction this one belongs to. An inner instruction has the same index as its top-level ancestor. |
absolute_ | The 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_ equals stack_, and absolute_ equals index. The pair (transaction signature, absolute_) identifies an instruction uniquely, which is how DeduplicationFilter recognizes repeats.
A Jupiter route that swaps through Meteora DLMM looks like this (simplified):
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:
- Walks the top-level instructions in order and, after each one, its group of inner instructions.
- Derives each inner instruction's
absolute_from the sequence of stack heights.path - Resolves account keys, including those loaded from address lookup tables, and builds a
solana_for each.instruction:: Instruction - Nests the list into
NestedInstructions, a tree ofNestedInstructionnodes.
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#
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. holds the direct children of the current instruction; each child's inner_ holds its own children. The children are raw instructions: decode them with any decoder by calling decode_.
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::. 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_ to get the swap's actual output amount.
Log events#
Anchor's emit! publishes an event as a Program data: < line in the transaction's log. InstructionMetadata:: returns the events logged by one instruction:
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_ (excluding its CPIs, and accounting for precompile instructions, which don't log), base64-decodes them, and keeps those that T:: 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:
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:
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.
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_loggeruse {
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
}cargo runswap 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.