1. API reference

Macros

The macros Carbon 2.0.0 exports: try_decode_instructions!, instruction_decoder_collection! and #[derive(InstructionType)], with their exact expansion and requirements.

On this page

Carbon exports three macros, used mostly by generated decoders and by code that combines several programs in one transaction pipe. This page documents what each one expects and what it generates in 2.0.0.

MacroKindDefined inUse it to
try_decode_instructions!declarativecarbon-macrosWrite an InstructionDecoder that tries each instruction type in turn
#[derive(InstructionType)]derivecarbon-proc-macrosGenerate a tag enum and get_instruction_type() for an instruction enum
instruction_decoder_collection!function-like proceduralcarbon-proc-macrosGenerate an InstructionDecoderCollection over several decoders

All three are re-exported from the root of carbon_core when its default macros feature is on, so import them as carbon_core::try_decode_instructions, carbon_core::InstructionType and carbon_core::instruction_decoder_collection. You don't need to depend on carbon-macros or carbon-proc-macros directly. With carbon-core = { version = "2.0.0", default-features = false } the macros are not available.

Examples on this page use:

Cargo.toml
[dependencies]
borsh = { version = "1.5.7", features = ["derive"] }
carbon-core = "2.0.0"
serde = { version = "1", features = ["derive"] }
solana-instruction = "3.4.1"
solana-pubkey = "4.2.1"

try_decode_instructions!#

Tries a list of instruction types against one instruction and returns the first that decodes.

Rust
try_decode_instructions!(
    $instruction:expr,
    $program_id:expr,
    $( $Enum::$Variant => $Type ),* $(,)?
)

Details. For each Enum::Variant => Type pair, in order, the macro expands to:

Rust
if let Some(data) = <Type>::decode(instruction.data.as_slice()) {
    if let Some(accounts) = <Type>::arrange_accounts(&instruction.accounts) {
        return Some(Enum::Variant { program_id: $program_id, data, accounts });
    }
}

and ends with None. Requirements:

RequirementWhy
Use it as the tail expression of a function returning Option<Enum>It uses return Some(…) on a match.
Each Type has an associated fn decode(&[u8]) -> Option<Self>Called as <Type>::decode. An inherent method works; CarbonDeserialize::deserialize does not (different name). The macro's own doc comment says CarbonDeserialize is required, which is wrong in 2.0.0.
Each Type implements ArrangeAccountsCalled as <Type>::arrange_accounts. The macro imports the trait itself.
Each Enum::Variant is a struct variant with fields program_id, data, accountsBuilt with those field names.
The crate carbon_core is reachable by that nameThe expansion uses carbon_core::deserialize::ArrangeAccounts.

An instruction whose data decodes but whose account list is too short is skipped, and the next pair is tried. Order matters when discriminators overlap. Generated decoders call this macro from decode_instruction after checking the program ID.

Example.

src/vault.rs
use {
    carbon_core::{
        account_utils::next_account, deserialize::ArrangeAccounts,
        instruction::InstructionDecoder, try_decode_instructions,
    },
    solana_instruction::{AccountMeta, Instruction},
    solana_pubkey::Pubkey,
};

pub const PROGRAM_ID: Pubkey =
    Pubkey::from_str_const("Vau1t11111111111111111111111111111111111111");

#[derive(Debug, Clone, borsh::BorshDeserialize)]
pub struct Deposit {
    pub amount: u64,
}

#[derive(Debug, Clone)]
pub struct DepositAccounts {
    pub vault: Pubkey,
    pub user: Pubkey,
}

impl Deposit {
    pub fn decode(data: &[u8]) -> Option<Self> {
        let body = data.strip_prefix(&[0u8])?;
        borsh::from_slice(body).ok()
    }
}

impl ArrangeAccounts for Deposit {
    type ArrangedAccounts = DepositAccounts;

    fn arrange_accounts(accounts: &[AccountMeta]) -> Option<DepositAccounts> {
        let mut iter = accounts.iter();
        Some(DepositAccounts {
            vault: next_account(&mut iter)?,
            user: next_account(&mut iter)?,
        })
    }
}

#[derive(Debug, Clone, borsh::BorshDeserialize)]
pub struct Withdraw {
    pub amount: u64,
}

impl Withdraw {
    pub fn decode(data: &[u8]) -> Option<Self> {
        let body = data.strip_prefix(&[1u8])?;
        borsh::from_slice(body).ok()
    }
}

impl ArrangeAccounts for Withdraw {
    type ArrangedAccounts = DepositAccounts;

    fn arrange_accounts(accounts: &[AccountMeta]) -> Option<DepositAccounts> {
        Deposit::arrange_accounts(accounts)
    }
}

#[derive(Debug, Clone)]
pub enum VaultInstruction {
    Deposit { program_id: Pubkey, data: Deposit, accounts: DepositAccounts },
    Withdraw { program_id: Pubkey, data: Withdraw, accounts: DepositAccounts },
}

pub struct VaultDecoder;

impl InstructionDecoder<'_> for VaultDecoder {
    type InstructionType = VaultInstruction;

    fn decode_instruction(&self, instruction: &Instruction) -> Option<VaultInstruction> {
        if instruction.program_id != PROGRAM_ID {
            return None;
        }
        try_decode_instructions!(
            instruction,
            PROGRAM_ID,
            VaultInstruction::Deposit => Deposit,
            VaultInstruction::Withdraw => Withdraw,
        )
    }
}

See also. InstructionDecoder, Custom decoder, source, docs.rs.

#[derive(InstructionType)]#

Generates a tag enum with the same variant names and a get_instruction_type method.

Rust
#[derive(InstructionType)]
pub enum MyInstruction { /* variants */ }

// generates:
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub enum MyInstructionType { /* same variant names, no fields */ }

impl MyInstruction {
    pub fn get_instruction_type(&self) -> MyInstructionType;
}

Details. Works with unit, tuple and struct variants; fields are dropped in the tag enum. The tag enum is always pub and named <Enum>Type, and its derive uses the path serde::Serialize, so your crate must depend on serde. Generics on the source enum are not supported. Generated decoders in 2.0.0 don't use this derive, so MeteoraDlmmInstructionType and similar don't exist.

Example.

Rust
use carbon_core::InstructionType;

#[derive(Debug, Clone, InstructionType)]
pub enum CounterInstruction {
    Initialize,
    Increment(u64),
    Reset { to: u64 },
}

pub fn demo() {
    let ix = CounterInstruction::Increment(5);
    assert_eq!(ix.get_instruction_type(), CounterInstructionType::Increment);
}

See also. instruction_decoder_collection!, docs.rs.

instruction_decoder_collection!#

Generates an enum over several programs' instructions and its InstructionDecoderCollection impl.

Rust
instruction_decoder_collection!(
    InstructionsEnum, InstructionTypesEnum, ProgramsEnum,
    // 4-part entry
    Variant => PROGRAM_ID_PATH => decoder_expr => InstructionType,
    // 3-part entry
    Variant => decoder_expr => InstructionType,
    …
);

Details. The macro generates three pub enums and one impl:

Generated itemDefinition
InstructionsEnum#[derive(Debug, Clone, Hash, serde::Serialize, serde::Deserialize, PartialEq, Eq)], one tuple variant Variant(InstructionType) per entry
InstructionTypesEnum#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)], one variant Variant(InstructionTypeType) per entry, wrapping the tag from #[derive(InstructionType)]
ProgramsEnum#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)], one unit variant per entry. Not used by anything; available for your code.
impl carbon_core::collection::InstructionDecoderCollection for InstructionsEnumtype InstructionType = InstructionTypesEnum. parse_instruction matches instruction.program_id against every 4-part PROGRAM_ID_PATH; on a match it returns that decoder's result (or None, without trying others). When no 4-part path matches, it tries the 3-part decoders in declaration order. get_type calls get_instruction_type() on the inner value.

Requirements, each of which is a compile error when missing:

  1. Each InstructionType derives Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize and InstructionType.
  2. The InstructionType path's last segment is used as a bare name, and so is <Name>Type: both must be in scope (import them with use).
  3. PROGRAM_ID_PATH must be a const path (it is used as a match pattern), not a static or an expression.
  4. decoder_expr is an expression evaluated on every call (MemoDecoder, or MyDecoder::new()), and must implement InstructionDecoder.
  5. Your crate depends on serde and solana-instruction, which the expansion names directly.

Generated decoders don't meet requirement 1: their instruction enums derive neither Eq, Hash nor InstructionType. Passing MeteoraDlmmInstruction fails with:

Text
error[E0277]: the trait bound `MeteoraDlmmInstruction: Eq` is not satisfied
error[E0277]: the trait bound `MeteoraDlmmInstruction: Hash` is not satisfied
error[E0425]: cannot find type `MeteoraDlmmInstructionType` in this scope
error[E0599]: no method named `get_instruction_type` found for reference `&MeteoraDlmmInstruction` in the current scope

For generated decoders, write the collection by hand as shown in InstructionDecoderCollection.

Example. Two hand-written decoders, one 4-part and one 3-part entry:

src/main.rs
use {
    carbon_core::{instruction::InstructionDecoder, instruction_decoder_collection, InstructionType},
    serde::{Deserialize, Serialize},
    solana_instruction::Instruction,
    solana_pubkey::Pubkey,
};

pub const MEMO_PROGRAM_ID: Pubkey =
    Pubkey::from_str_const("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr");
pub const NOOP_PROGRAM_ID: Pubkey =
    Pubkey::from_str_const("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV");

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, InstructionType)]
pub enum MemoInstruction {
    Memo(String),
}

pub struct MemoDecoder;

impl InstructionDecoder<'_> for MemoDecoder {
    type InstructionType = MemoInstruction;

    fn decode_instruction(&self, ix: &Instruction) -> Option<MemoInstruction> {
        (ix.program_id == MEMO_PROGRAM_ID)
            .then(|| String::from_utf8(ix.data.clone()).ok())
            .flatten()
            .map(MemoInstruction::Memo)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, InstructionType)]
pub enum NoopInstruction {
    Log(Vec<u8>),
}

pub struct NoopDecoder;

impl InstructionDecoder<'_> for NoopDecoder {
    type InstructionType = NoopInstruction;

    fn decode_instruction(&self, ix: &Instruction) -> Option<NoopInstruction> {
        (ix.program_id == NOOP_PROGRAM_ID).then(|| NoopInstruction::Log(ix.data.clone()))
    }
}

instruction_decoder_collection!(
    AllInstructions, AllInstructionTypes, AllPrograms,
    // 4-part: matched on the program ID first.
    Memo => MEMO_PROGRAM_ID => MemoDecoder => MemoInstruction,
    // 3-part: tried in order when no 4-part entry matches.
    Noop => NoopDecoder => NoopInstruction,
);

Add a main to the same file that parses one instruction for each program:

Rust
use carbon_core::collection::InstructionDecoderCollection;

fn main() {
    let memo = Instruction {
        program_id: MEMO_PROGRAM_ID,
        accounts: vec![],
        data: b"gm".to_vec(),
    };
    let noop = Instruction {
        program_id: NOOP_PROGRAM_ID,
        accounts: vec![],
        data: vec![1, 2, 3],
    };

    for ix in [memo, noop] {
        let parsed = AllInstructions::parse_instruction(&ix).unwrap();
        println!("{parsed:?} -> {:?}", parsed.get_type());
    }
}
Terminal
cargo run
Output
Memo(Memo("gm")) -> Memo(Memo)
Noop(Log([1, 2, 3])) -> Noop(Log)

Register the collection on a pipeline with Pipeline::builder().transaction::<AllInstructions, _>(processor).

See also. Transactions, Multiple programs, source, docs.rs.

Re-exported crates#

carbon_core also re-exports two crates that generated code relies on.

Rust
pub use borsh;
#[doc(hidden)]
pub use log; // only with the `macros` feature

Details. carbon_core::borsh is borsh 1.x. Generated decoders derive carbon_core::borsh::BorshDeserialize with use carbon_core::borsh; in scope, so a decoder crate doesn't need its own borsh dependency for those derives. carbon_core::log is hidden from the docs and not part of the public API; depend on log yourself.

Example.

Rust
use carbon_core::borsh::{self, BorshDeserialize};

#[derive(BorshDeserialize)]
pub struct Header {
    pub version: u8,
}

See also. Feature flags, docs.rs.

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.