Processor inputs
Every type a Carbon 2.0.0 processor receives, field by field, for account, instruction, transaction, deletion and block-details pipes.
On this page
A processor implements Processor< for the input type of the pipe it is registered on. This page lists each input type and every field in it, so you know what data your process method can read. For how to write a processor, see Processors.
| Builder method | Processor implements | Defined in |
|---|---|---|
account | Processor< | carbon_ |
instruction | Processor< | carbon_ |
transaction | Processor< | carbon_ |
account_ | Processor< | carbon_ (fields) |
block_ | Processor< | carbon_ (fields) |
Every input is passed by reference and borrows data the pipeline owns for the duration of one process call. Clone what you need to keep.
Example#
This program prints the instruction input for each Meteora DLMM Swap2 the transaction crawler finds on the public RPC endpoint.
[dependencies]
carbon-core = "2.0.0"
carbon-meteora-dlmm-decoder = "2.0.0"
carbon-rpc-transaction-crawler-datasource = "2.0.0"
tokio = { version = "1", features = ["full"] }
tokio-util = "0.7"use {
carbon_core::{
error::CarbonResult, instruction::InstructionProcessorInputType, pipeline::Pipeline,
processor::Processor,
},
carbon_meteora_dlmm_decoder::{
instructions::MeteoraDlmmInstruction, MeteoraDlmmDecoder, PROGRAM_ID,
},
carbon_rpc_transaction_crawler_datasource::{ConnectionConfig, Filters, RpcTransactionCrawler},
std::time::Duration,
tokio_util::sync::CancellationToken,
};
struct InputPrinter;
impl Processor<InstructionProcessorInputType<'_, MeteoraDlmmInstruction>> for InputPrinter {
async fn process(
&mut self,
input: &InstructionProcessorInputType<'_, MeteoraDlmmInstruction>,
) -> CarbonResult<()> {
let MeteoraDlmmInstruction::Swap2 { data, accounts, .. } = input.decoded_instruction else {
return Ok(());
};
let meta = input.metadata;
let tx = &meta.transaction_metadata;
println!("signature: {}", tx.signature);
println!("slot: {} (block_time {:?})", tx.slot, tx.block_time);
println!("fee payer: {}", tx.fee_payer);
println!("stack height: {}", meta.stack_height);
println!("index: {}", meta.index);
println!("absolute path: {:?}", meta.absolute_path);
println!("inner ixs: {}", input.nested_instructions.len());
println!("raw accounts: {}", input.raw_instruction.accounts.len());
println!("lb_pair: {}", accounts.lb_pair);
println!("amount_in: {}", data.amount_in);
println!();
Ok(())
}
}
#[tokio::main]
async fn main() -> CarbonResult<()> {
let datasource = RpcTransactionCrawler::new(
"https://api.mainnet-beta.solana.com".to_string(),
PROGRAM_ID,
ConnectionConfig::default(),
Filters::new(None, None, None),
None,
);
let token = CancellationToken::new();
let stop = token.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(90)).await;
stop.cancel();
});
Pipeline::builder()
.datasource(datasource)
.instruction(MeteoraDlmmDecoder, InputPrinter)
.datasource_cancellation_token(token)
.build()?
.run()
.await
}cargo runThe first swap is a top-level instruction; the second is a CPI from another program, so its stack_ is 2 and its index is the index of the top-level instruction that called it:
signature: 43eGCzcyUFYtJsuoWwmFsirBBu51tm6BRs7WiCWYRfR7txoz1DATD5iSQNFfS1LXuGJvYG2AXTA3t38N1DG9eYJp
slot: 467120934 (block_time Some(1794185094))
fee payer: 6TWMHcW3gGXRnzRoe6vTgENjJhCbTFApcA5yLEJJaBDh
stack height: 1
index: 2
absolute path: [2]
inner ixs: 4
raw accounts: 17
lb_pair: GeUkx21Vc6yg63YZ1BdXY95ZATm9LeBYCgSN1uJ3o18S
amount_in: 404287484
signature: 2W5fitdRrbLo2pNF4ChwDPPcmRNC3EfR1u7viGGyPXdnHZM8HBu2dFnJbVxktupvJKbXcxheZaadphndEMkCfNTi
slot: 467120933 (block_time Some(1794185094))
fee payer: AgmLJBMDCqWynYnQiPCuj9ewsNNsBJXyzoUhD9LJzN51
stack height: 2
index: 1
absolute path: [1, 1]
inner ixs: 4
raw accounts: 19
lb_pair: CcG3fyDZn6uXTV5p2TV9cceCA2YhE3bUMLeXGn6vZSNr
amount_in: 680804450AccountProcessorInputType#
What an account processor receives for each account its decoder accepts.
#[derive(Debug)]
pub struct AccountProcessorInputType<'a, T> {
pub metadata: &'a AccountMetadata,
pub decoded_account: &'a DecodedAccount<T>,
pub raw_account: &'a solana_account::Account,
}Details.
| Field | Type | Meaning |
|---|---|---|
metadata | &AccountMetadata | Slot, address and writing transaction. See AccountMetadata. |
decoded_ | &DecodedAccount< | Decoder output. T is the decoder's AccountType, for example MeteoraDlmmAccount. |
raw_account | &solana_ | The account exactly as the datasource sent it, including the undecoded data bytes. |
Example.
impl Processor<AccountProcessorInputType<'_, MeteoraDlmmAccount>> for PoolLogger {
async fn process(
&mut self,
input: &AccountProcessorInputType<'_, MeteoraDlmmAccount>,
) -> CarbonResult<()> {
if let MeteoraDlmmAccount::LbPair(pool) = &input.decoded_account.data {
println!(
"{} active_id={} ({} bytes, slot {})",
input.metadata.pubkey,
pool.active_id,
input.raw_account.data.len(),
input.metadata.slot
);
}
Ok(())
}
}AccountMetadata#
Where and when an account update was observed.
#[derive(Debug, Clone)]
pub struct AccountMetadata {
pub slot: u64,
pub pubkey: Pubkey,
pub transaction_signature: Option<Signature>,
}Details. Copied from the AccountUpdate.
| Field | Type | Meaning |
|---|---|---|
slot | u64 | Slot of the update. |
pubkey | solana_ | Address of the account. |
transaction_ | Option< | Transaction that wrote this state. Only Yellowstone gRPC and Helius LaserStream set it; None everywhere else. |
Example.
let key = (input.metadata.pubkey, input.metadata.slot);See also. docs.rs.
DecodedAccount#
An account decoder's output: the typed data plus the account header.
#[derive(Debug, Clone)]
pub struct DecodedAccount<T> {
pub lamports: u64,
pub data: T,
pub owner: Pubkey,
pub executable: bool,
pub rent_epoch: u64,
}Details.
| Field | Type | Meaning |
|---|---|---|
lamports | u64 | Balance. Always above 0 here: zero-lamport accounts arrive as deletions. |
data | T | The decoded account. Generated decoders use an enum with one boxed variant per account type, for example MeteoraDlmmAccount::. |
owner | solana_ | Owning program. |
executable | bool | Whether the account is a program. |
rent_epoch | u64 | Rent epoch from the account. |
Generated decoders copy the four header fields from the raw account. A hand-written AccountDecoder builds this struct itself.
Example.
let decoded = &input.decoded_account;
println!("owner={} lamports={}", decoded.owner, decoded.lamports);See also. docs.rs.
InstructionProcessorInputType#
What an instruction processor receives for each instruction its decoder accepts.
#[derive(Debug)]
pub struct InstructionProcessorInputType<'a, T> {
pub metadata: &'a InstructionMetadata,
pub decoded_instruction: &'a T,
pub nested_instructions: &'a NestedInstructions,
pub raw_instruction: &'a solana_instruction::Instruction,
}Details.
| Field | Type | Meaning |
|---|---|---|
metadata | &InstructionMetadata | Position in the transaction and the shared transaction data. See InstructionMetadata. |
decoded_ | &T | Decoder output. For generated decoders, an enum with struct variants: MeteoraDlmmInstruction::. |
nested_ | &NestedInstructions | This instruction's own CPIs (its subtree), not the whole transaction. Empty when it made no CPIs. |
raw_ | &solana_ | The instruction with program_id, resolved accounts (AccountMeta with is_signer and is_writable) and raw data. Lookup-table accounts of V0 transactions are already resolved from meta.. |
The pipe runs once per matching instruction, top-level or inner. If a program calls your program through CPI, you receive that inner instruction with stack_.
Example.
for child in input.nested_instructions.iter() {
println!("CPI to {}", child.instruction.program_id);
}See also. instruction, Instructions, docs.rs.
InstructionMetadata#
Where an instruction sits in its transaction.
#[derive(Debug, Clone)]
pub struct InstructionMetadata {
pub transaction_metadata: Arc<TransactionMetadata>,
pub stack_height: u32,
pub index: u32,
pub absolute_path: Vec<u8>,
}Details.
| Field | Type | Meaning |
|---|---|---|
transaction_ | Arc< | The transaction this instruction belongs to, shared by all its instructions. See TransactionMetadata. |
stack_ | u32 | 1 for a top-level instruction, 2 for a CPI made by it, up to 5 (MAX_). |
index | u32 | Index of the top-level instruction in the message. Inner instructions carry the index of the top-level instruction they descend from, not their position among siblings. |
absolute_ | Vec<u8> | Path from the root of the instruction tree: [top_, one entry per stack level, with positions counted from 0. [2] is the third top-level instruction; [1, 1] is the second CPI under the second top-level instruction. Unique within a transaction. |
Use (transaction_ as a unique key for an instruction; (signature, index, stack_ is not unique when one instruction makes several CPIs at the same depth.
Carbon reads inner instructions from meta. and their depth from each inner instruction's stack_. When the source reports a missing or out-of-range stack_ (outside 2 to 5), Carbon logs invalid inner instruction stack height (…), dropping the remaining inner instructions of this group at warn level and skips the rest of that group.
Example.
let key = (
input.metadata.transaction_metadata.signature,
input.metadata.absolute_path.clone(),
);InstructionMetadata:: decode_ log_ events#
Decodes the program log events emitted by this instruction.
pub fn decode_log_events<T: CarbonDeserialize>(&self) -> Vec<T>Details. Walks transaction_, tracks Program … invoke [n] and Program … success / failed lines to follow the call stack, and keeps the log lines emitted while this instruction (matched by absolute_) was executing. For each such line, it base64-decodes the last whitespace-separated token and calls T::; lines that fail to decode or deserialize are skipped. This targets Anchor emit! events, logged as Program data: <. Logs emitted by this instruction's own CPIs are not included.
Precompile instructions (Ed25519, Secp256k1, Secp256r1) log nothing, so Carbon subtracts the number of precompiles before this instruction from the top-level index before matching. Returns an empty Vec when the transaction has no log messages. If the runtime truncated the logs (Log truncated), events after the cut are missing.
Generated decoders in 2.0.0 do not implement CarbonDeserialize for their event types; they expose an inherent decode method instead, and Anchor events emitted through self-CPI (emit_cpi!) arrive as a decoded CpiEvent instruction. Implement CarbonDeserialize yourself to use this method; see CarbonDeserialize.
Example.
for event in input.metadata.decode_log_events::<MyEvent>() {
println!("{} moved {}", event.pool, event.amount);
}See also. Instructions, docs.rs.
NestedInstruction#
One node in a transaction's instruction tree.
#[derive(Debug, Clone)]
pub struct NestedInstruction {
pub metadata: InstructionMetadata,
pub instruction: solana_instruction::Instruction,
pub inner_instructions: NestedInstructions,
}Details.
| Field | Type | Meaning |
|---|---|---|
metadata | InstructionMetadata | Position of this instruction. |
instruction | solana_ | The raw instruction. It is not decoded: run a decoder on it yourself if you need its typed form. |
inner_ | NestedInstructions | The CPIs this instruction made, recursively. |
Example.
fn count(instructions: &NestedInstructions) -> usize {
instructions
.iter()
.map(|ix| 1 + count(&ix.inner_instructions))
.sum()
}See also. docs.rs.
NestedInstructions#
An ordered list of NestedInstructions.
#[derive(Debug, Default)]
pub struct NestedInstructions(pub Vec<NestedInstruction>);
impl NestedInstructions {
pub fn len(&self) -> usize;
pub fn is_empty(&self) -> bool;
pub fn push(&mut self, nested_instruction: NestedInstruction);
}Details. Derefs to [NestedInstruction], so slice methods such as iter(), first() and indexing work. It also implements Clone, owned IntoIterator, and From<, which builds the tree from a flat Vec< ordered top-level-first as the runtime reports it. Filters receive the whole transaction's tree in filter_; instruction processors receive one subtree in nested_.
From< asserts that every stack_ is between 1 and MAX_ (5) and panics otherwise. The pipeline only builds trees from validated data, but if you call it yourself, check heights first.
Example.
if let Some(first) = input.nested_instructions.first() {
println!("first CPI: {:?}", first.metadata.absolute_path);
}See also. docs.rs.
TransactionProcessorInputType#
What a transaction processor receives, once per transaction.
#[derive(Debug)]
pub struct TransactionProcessorInputType<'a, T> {
pub metadata: &'a Arc<TransactionMetadata>,
pub instructions: &'a [(InstructionMetadata, T)],
}Details.
| Field | Type | Meaning |
|---|---|---|
metadata | &Arc< | The transaction. See TransactionMetadata. |
instructions | &[(InstructionMetadata, T)] | Every instruction that T:: recognized, top-level and inner, in execution order (each top-level instruction followed by its CPIs). Unrecognized instructions are omitted. T is your InstructionDecoderCollection. |
The processor runs for every transaction the datasource sends, including transactions where instructions is empty. There is no nested tree here; use InstructionMetadata:: and absolute_ to rebuild parent-child relations.
Example.
impl Processor<TransactionProcessorInputType<'_, AllInstructions>> for TxLogger {
async fn process(
&mut self,
input: &TransactionProcessorInputType<'_, AllInstructions>,
) -> CarbonResult<()> {
if input.instructions.is_empty() {
return Ok(());
}
println!(
"{}: {} known instructions",
input.metadata.signature,
input.instructions.len()
);
Ok(())
}
}See also. transaction, Transactions, docs.rs.
TransactionMetadata#
Data shared by every instruction of one transaction.
#[derive(Debug, Clone, Default)]
pub struct TransactionMetadata {
pub slot: u64,
pub signature: Signature,
pub fee_payer: Pubkey,
pub meta: solana_transaction_status::TransactionStatusMeta,
pub message: solana_message::VersionedMessage,
pub index: Option<u64>,
pub block_time: Option<i64>,
pub block_hash: Option<Hash>,
}Details. Built from the TransactionUpdate with TryFrom, which fails with MissingFeePayer when the message has no account keys.
| Field | Type | Meaning |
|---|---|---|
slot | u64 | Slot the transaction landed in. |
signature | solana_ | Transaction signature. |
fee_payer | solana_ | First static account key of the message. |
meta | TransactionStatusMeta | Status, fee, balances, inner instructions, logs, token balances, rewards, loaded addresses, return data, compute units. For RPC datasources Carbon converts it from the JSON-RPC form: token balances without an owner or programId are dropped, and return_ holds the base64 text as bytes rather than the decoded bytes. |
message | solana_ | The full message (legacy, V0 or V1), including recent_, static account keys and compiled instructions. |
index | Option<u64> | Position in the block, when the datasource knows it. None for the RPC transaction crawler and Helius Atlas. |
block_time | Option<i64> | Unix timestamp in seconds, when provided. |
block_hash | Option< | Only set by the RPC block crawler and RPC block subscribe. |
Check meta. to detect failed transactions. Whether you receive failed transactions depends on the datasource: the RPC transaction crawler, RPC block crawler and RPC block subscribe drop them before they reach the pipeline; Yellowstone and LaserStream follow your subscription's failed filter; Helius GTFA follows its status filter.
Example.
let tx = &input.metadata.transaction_metadata;
if tx.meta.status.is_err() {
return Ok(());
}
println!("{} paid {} lamports", tx.fee_payer, tx.meta.fee);See also. docs.rs.
MAX_ INSTRUCTION_ STACK_ DEPTH#
The deepest instruction nesting Carbon accepts.
pub const MAX_INSTRUCTION_STACK_DEPTH: usize = 5;Details. Matches the Agave runtime's limit of a top-level instruction plus four levels of CPI. Inner instructions reported deeper than this are dropped with a warning.
Example.
assert!(input.metadata.stack_height as usize <= MAX_INSTRUCTION_STACK_DEPTH);See also. docs.rs.
Deletions and block details#
Account-deletion and block-details processors receive the update itself, with no metadata wrapper and no decoder:
impl Processor<AccountDeletion> for DeletionLogger {
async fn process(&mut self, deletion: &AccountDeletion) -> CarbonResult<()> {
println!("closed {} (owner {})", deletion.pubkey, deletion.account.owner);
Ok(())
}
}
impl Processor<BlockDetails> for BlockLogger {
async fn process(&mut self, block: &BlockDetails) -> CarbonResult<()> {
println!("block {} at {:?}", block.slot, block.block_time);
Ok(())
}
}See AccountDeletion and BlockDetails for their fields.