Decoders
How AccountDecoder and InstructionDecoder turn raw bytes into Rust types, how discriminators and ArrangeAccounts work, and how decoder crates are laid out.
On this page
A decoder recognizes the accounts or instructions of one program and turns their bytes into Rust types. Every account and instruction pipe has one. This page covers the two decoder traits, how decoding works, how a generated decoder crate is organized and where decoders come from.
The decoder traits#
pub trait AccountDecoder<'a> {
type AccountType;
fn decode_account(
&self,
account: &'a solana_account::Account,
) -> Option<DecodedAccount<Self::AccountType>>;
}
pub trait InstructionDecoder<'a> {
type InstructionType;
fn decode_instruction(
&self,
instruction: &'a solana_instruction::Instruction,
) -> Option<Self::InstructionType>;
}Both return None for data that isn't theirs. The pipe then skips the update and the processor isn't called, so a pipe with a Meteora decoder ignores every non-Meteora instruction in a transaction. A decoder that returns Some for everything sees every update of its kind.
DecodedAccount< carries the decoded data plus the account header:
pub struct DecodedAccount<T> {
pub lamports: u64,
pub data: T,
pub owner: Pubkey,
pub executable: bool,
pub rent_epoch: u64,
}The instruction passed to decode_ is a solana_ with the program ID, the account metas (pubkey, signer, writable) and the data. Carbon builds it from the transaction and resolves address lookup tables first, so account keys are complete. Pass a decoder to the pipeline by value: . or ..
How decoding works#
Discriminators#
Programs put a discriminator at the start of their data to say what it is:
- Anchor programs use 8 bytes: the first 8 bytes of the SHA-256 of
global:<for instructions,instruction_ name> account:<for accounts andAccountName> event:<for events. Meteora DLMM'sEventName> swapinstruction starts with[248, 198, 158, 145, 225, 117, 135, 200]. - Native and SPL programs use their own layouts. SPL Token instructions start with a one-byte tag (
Transferis3), and token accounts have no discriminator: the decoder recognizes them by size.
A generated decoder gives each account, instruction and event type a decode(data: &[u8]) -> Option< method that checks the discriminator and deserializes the rest with Borsh, ignoring trailing bytes. The program's decode_ first checks instruction., then tries each instruction type in turn; decode_ first checks account., then tries each account type. The first match wins.
CarbonDeserialize#
carbon_ is the trait form of the same idea, for types you write yourself:
pub trait CarbonDeserialize
where
Self: Sized + borsh::BorshDeserialize,
{
const DISCRIMINATOR: &'static [u8];
fn deserialize(data: &[u8]) -> Option<Self>;
}decode_ needs it to decode events from transaction logs. Generated decoders in 2.0.0 don't implement it; they use the inherent decode methods above. The same module provides extract_, which splits data into (discriminator, rest).
ArrangeAccounts#
An instruction's accounts arrive as a list in the order the program expects them. ArrangeAccounts maps that list to a struct with named fields:
pub trait ArrangeAccounts {
type ArrangedAccounts: Clone + Send + Sync + std::fmt::Debug;
fn arrange_accounts(accounts: &[solana_instruction::AccountMeta]) -> Option<Self::ArrangedAccounts>;
}Generated instruction types implement it. Optional accounts in the IDL become Option<, and any accounts beyond the IDL's list land in a remaining: Vec< field. If the instruction has fewer accounts than it requires, arrange_ returns None and the instruction isn't decoded.
Generated decoder crates#
Every published decoder, and every decoder the CLI generates, has the same layout. For Meteora DLMM (carbon-meteora-dlmm-decoder):
- src/
- lib.rsMeteoraDlmmDecoder, PROGRAM_ID
- accounts/one module per account type, MeteoraDlmmAccount, AccountDecoder impl
- instructions/one module per instruction, MeteoraDlmmInstruction, InstructionDecoder impl
- events/one module per event type, each with decode()
- types/structs and enums the IDL defines and the others use
- graphql/behind the graphql feature
| Item | What it is |
|---|---|
MeteoraDlmmDecoder | A unit struct that implements both AccountDecoder and InstructionDecoder. |
PROGRAM_ID | The program's address, a const Pubkey. Use it to configure datasources. |
accounts:: | One variant per account type, such as LbPair(Box<. |
instructions:: | One struct variant per instruction: Swap { program_, where data holds the arguments (Swap { amount_) and accounts the named accounts (SwapInstructionAccounts). |
events:: | An event payload, with its own decode. |
accounts::, instructions:: | Row types and migrations, behind the postgres feature. |
The account and instruction enums derive Debug, Clone and PartialEq, plus Serialize and Deserialize with the serde feature. Decoders generated by CLI 2.0.0 for programs with Anchor events also export EVENT_; the published 2.0.0 crates predate that and don't. The decoder catalog documents each published crate.
Events and CPI events#
Anchor programs publish events in one of two ways, and Carbon decodes each differently:
- Event CPI (
emit_cpi!): the program calls itself with an instruction whose data is the 8-byte event-CPI tag[228, 69, 165, 46, 81, 203, 154, 29], the event's discriminator and the event. A generated decoder decodes this inner instruction as aCpiEventvariant:MeteoraDlmmInstruction::. It reaches your instruction processor like any other instruction, and also appears among the parent instruction's nested instructions.CpiEvent { data: CpiEvent:: Swap(SwapEvent { . . }), . . } - Log events (
emit!): the program writes aProgram data: <log line. Carbon doesn't turn these into instructions; decode them from the instruction's metadata withbase64> decode_.log_ events
Instructions shows both in a processor.
Where decoders come from#
- Published crates. Carbon 2.0 publishes 63 decoder crates,
carbon-<, for SPL Token, Token-2022, System, Jupiter, Raydium, Orca, Meteora, Pump.fun, Kamino and more. See the catalog.program>-decoder - Generated with the CLI.
carbon-cli parseturns an Anchor or Codama IDL, or an on-chain IDL fetched by program address, into a decoder crate with this layout. See Generate a decoder. - Written by hand. For programs without an IDL, or when you only need one or two types, implement the traits yourself. See Write a custom decoder and the second example below.
Examples#
Decode an instruction with a generated decoder#
This program builds a Meteora DLMM swap instruction by hand and decodes it. In a pipeline, the instruction comes from a transaction instead.
cargo add carbon-core@2.0.0 carbon-meteora-dlmm-decoder@2.0.0
cargo add solana-instruction@3.4.1 solana-pubkey@4.2.1use {
carbon_core::instruction::InstructionDecoder,
carbon_meteora_dlmm_decoder::{
instructions::MeteoraDlmmInstruction, MeteoraDlmmDecoder, PROGRAM_ID,
},
solana_instruction::{AccountMeta, Instruction},
solana_pubkey::Pubkey,
};
fn main() {
// Anchor discriminator of `swap`, then the Borsh-encoded arguments.
let mut data = vec![248, 198, 158, 145, 225, 117, 135, 200];
data.extend_from_slice(&1_000_000u64.to_le_bytes()); // amount_in
data.extend_from_slice(&990_000u64.to_le_bytes()); // min_amount_out
let accounts = (0..15)
.map(|_| AccountMeta::new(Pubkey::new_unique(), false))
.collect();
let instruction = Instruction { program_id: PROGRAM_ID, accounts, data };
match MeteoraDlmmDecoder.decode_instruction(&instruction) {
Some(MeteoraDlmmInstruction::Swap { data, accounts, .. }) => println!(
"swap: amount_in={} min_amount_out={} lb_pair={}",
data.amount_in, data.min_amount_out, accounts.lb_pair
),
Some(_) => println!("another DLMM instruction"),
None => println!("not a DLMM instruction"),
}
}swap: amount_in=1000000 min_amount_out=990000 lb_pair=11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQsWrite an account decoder by hand#
This decoder reads a CounterState account of a hypothetical Anchor program: an 8-byte discriminator followed by a Borsh-encoded authority and count.
cargo add carbon-core@2.0.0 solana-account@4.3.2 solana-pubkey@4.2.1
cargo add borsh --features deriveuse {
borsh::BorshDeserialize,
carbon_core::{
account::{AccountDecoder, DecodedAccount},
deserialize::CarbonDeserialize,
},
solana_account::Account,
solana_pubkey::Pubkey,
};
const PROGRAM_ID: Pubkey = Pubkey::from_str_const("Counter111111111111111111111111111111111111");
/// On-chain layout: 8-byte discriminator, then Borsh fields.
#[derive(Debug, BorshDeserialize)]
pub struct CounterState {
pub authority: Pubkey,
pub count: u64,
}
impl CarbonDeserialize for CounterState {
// The first 8 bytes of sha256("account:CounterState")
const DISCRIMINATOR: &'static [u8] = &[98, 23, 205, 159, 14, 202, 79, 139];
fn deserialize(data: &[u8]) -> Option<Self> {
let mut body = data.strip_prefix(Self::DISCRIMINATOR)?;
// `deserialize` (not `try_from_slice`) tolerates trailing padding bytes.
<Self as BorshDeserialize>::deserialize(&mut body).ok()
}
}
pub struct CounterDecoder;
impl<'a> AccountDecoder<'a> for CounterDecoder {
type AccountType = CounterState;
fn decode_account(&self, account: &'a Account) -> Option<DecodedAccount<CounterState>> {
if account.owner != PROGRAM_ID {
return None;
}
Some(DecodedAccount {
lamports: account.lamports,
data: <CounterState as CarbonDeserialize>::deserialize(&account.data)?,
owner: account.owner,
executable: account.executable,
rent_epoch: account.rent_epoch,
})
}
}
fn main() {
let mut data = CounterState::DISCRIMINATOR.to_vec();
data.extend_from_slice(&[7; 32]); // authority
data.extend_from_slice(&42u64.to_le_bytes()); // count
data.extend_from_slice(&[0; 16]); // padding
let account = Account {
lamports: 1_224_960,
data,
owner: PROGRAM_ID,
executable: false,
rent_epoch: u64::MAX,
};
let decoded = CounterDecoder.decode_account(&account).expect("a CounterState account");
println!("{:?}", decoded.data);
}CounterState { authority: US517G5965aydkZ46HS38QLi7UQiSojurfbQfKCELFx, count: 42 }CounterDecoder works in a pipeline like a generated one: ..