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.
| Macro | Kind | Defined in | Use it to |
|---|---|---|---|
try_ | declarative | carbon-macros | Write an InstructionDecoder that tries each instruction type in turn |
#[derive(InstructionType)] | derive | carbon-proc-macros | Generate a tag enum and get_ for an instruction enum |
instruction_ | function-like procedural | carbon-proc-macros | Generate 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_, carbon_ and carbon_. You don't need to depend on carbon-macros or carbon-proc-macros directly. With carbon-core = { version = "2. the macros are not available.
Examples on this page use:
[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.
try_decode_instructions!(
$instruction:expr,
$program_id:expr,
$( $Enum::$Variant => $Type ),* $(,)?
)Details. For each Enum:: pair, in order, the macro expands to:
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:
| Requirement | Why |
|---|---|
Use it as the tail expression of a function returning Option< | It uses return Some(…) on a match. |
Each Type has an associated fn decode(&[u8]) -> Option< | Called as <. An inherent method works; CarbonDeserialize:: does not (different name). The macro's own doc comment says CarbonDeserialize is required, which is wrong in 2.0.0. |
Each Type implements ArrangeAccounts | Called as <. The macro imports the trait itself. |
Each Enum:: is a struct variant with fields program_id, data, accounts | Built with those field names. |
The crate carbon_core is reachable by that name | The expansion uses carbon_. |
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_ after checking the program ID.
Example.
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_ method.
#[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::, 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.
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_, docs.rs.
instruction_ decoder_ collection!#
Generates an enum over several programs' instructions and its InstructionDecoderCollection impl.
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 item | Definition |
|---|---|
InstructionsEnum | #[derive(Debug, Clone, Hash, serde::, one tuple variant Variant(InstructionType) per entry |
InstructionTypesEnum | #[derive(Debug, Clone, PartialEq, Eq, serde::, one variant Variant(InstructionTypeType) per entry, wrapping the tag from #[derive(InstructionType)] |
ProgramsEnum | #[derive(Debug, Clone, PartialEq, Eq, serde::, one unit variant per entry. Not used by anything; available for your code. |
impl carbon_ | type InstructionType = InstructionTypesEnum. parse_ matches instruction. against every 4-part PROGRAM_; 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_ on the inner value. |
Requirements, each of which is a compile error when missing:
- Each
InstructionTypederivesDebug,Clone,PartialEq,Eq,Hash,serde::,Serialize serde::andDeserialize InstructionType. - The
InstructionTypepath's last segment is used as a bare name, and so is<Name>Type: both must be in scope (import them withuse). PROGRAM_must be aID_ PATH constpath (it is used as amatchpattern), not astaticor an expression.decoder_is an expression evaluated on every call (expr MemoDecoder, orMyDecoder::), and must implementnew() InstructionDecoder.- Your crate depends on
serdeandsolana-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:
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 scopeFor generated decoders, write the collection by hand as shown in InstructionDecoderCollection.
Example. Two hand-written decoders, one 4-part and one 3-part entry:
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:
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());
}
}cargo runMemo(Memo("gm")) -> Memo(Memo)
Noop(Log([1, 2, 3])) -> Noop(Log)Register the collection on a pipeline with Pipeline::.
See also. Transactions, Multiple programs, source, docs.rs.
Re-exported crates#
carbon_core also re-exports two crates that generated code relies on.
pub use borsh;
#[doc(hidden)]
pub use log; // only with the `macros` featureDetails. carbon_ is borsh 1.x. Generated decoders derive carbon_ with use carbon_ in scope, so a decoder crate doesn't need its own borsh dependency for those derives. carbon_ is hidden from the docs and not part of the public API; depend on log yourself.
Example.
use carbon_core::borsh::{self, BorshDeserialize};
#[derive(BorshDeserialize)]
pub struct Header {
pub version: u8,
}See also. Feature flags, docs.rs.