Skip to main content

hopper_core/dispatch/
mod.rs

1//! Instruction dispatch -- tag-based routing with zero allocation.
2//!
3//! Hopper uses a 1-byte or 2-byte tag at the start of instruction data
4//! to route to handler functions. The `dispatch!` macro generates an
5//! efficient match statement.
6//!
7//! ## Dispatch variants
8//!
9//! - `hopper_dispatch!`, Standard: receives `(program_id, accounts, data)`
10//! - `hopper_dispatch_lazy!`, Lazy: receives `LazyContext`, parses accounts on-demand
11//! - `hopper_dispatch_8!`, 8-byte discriminator (Anchor/Quasar compatible)
12
13use hopper_runtime::error::ProgramError;
14
15/// Read a 1-byte dispatch tag from instruction data.
16#[inline(always)]
17pub fn dispatch_instruction(data: &[u8]) -> Result<(u8, &[u8]), ProgramError> {
18    if data.is_empty() {
19        return Err(ProgramError::InvalidInstructionData);
20    }
21    Ok((data[0], &data[1..]))
22}
23
24/// Read a 2-byte dispatch tag (for programs with >256 instructions).
25#[inline(always)]
26pub fn dispatch_instruction_u16(data: &[u8]) -> Result<(u16, &[u8]), ProgramError> {
27    if data.len() < 2 {
28        return Err(ProgramError::InvalidInstructionData);
29    }
30    let tag = u16::from_le_bytes([data[0], data[1]]);
31    Ok((tag, &data[2..]))
32}
33
34/// Read an 8-byte discriminator (Anchor/Quasar compatible).
35#[inline(always)]
36pub fn dispatch_instruction_8(data: &[u8]) -> Result<([u8; 8], &[u8]), ProgramError> {
37    if data.len() < 8 {
38        return Err(ProgramError::InvalidInstructionData);
39    }
40    let mut disc = [0u8; 8];
41    disc.copy_from_slice(&data[..8]);
42    Ok((disc, &data[8..]))
43}
44
45/// Event CPI prefix. Programs should check for this at dispatch entry
46/// and return `Ok(())` to allow self-CPI events to pass through.
47pub const EVENT_CPI_PREFIX: [u8; 2] = [0xFF, 0xFE];
48
49/// Macro for instruction dispatch.
50///
51/// ```ignore
52/// hopper_dispatch! {
53///     program_id, accounts, instruction_data;
54///     0 => process_init,
55///     1 => process_deposit,
56///     2 => process_withdraw,
57/// }
58/// ```
59#[macro_export]
60macro_rules! hopper_dispatch {
61    (
62        $program_id:expr, $accounts:expr, $data:expr;
63        $( $tag:literal => $handler:expr ),+ $(,)?
64    ) => {{
65        // Allow event CPI passthrough: if the data starts with the event
66        // prefix [0xFF, 0xFE], silently succeed so self-CPI events work.
67        if $data.len() >= 2 && $data[0] == 0xFF && $data[1] == 0xFE {
68            return Ok(());
69        }
70        let (tag, remaining) = $crate::dispatch::dispatch_instruction($data)?;
71        match tag {
72            $( $tag => $handler($program_id, $accounts, remaining), )+
73            _ => Err($crate::__runtime::error::ProgramError::InvalidInstructionData),
74        }
75    }};
76}
77
78/// Lazy dispatch -- routes on instruction data before parsing any accounts.
79///
80/// Each handler receives a `&mut LazyContext` and can parse only the accounts
81/// it needs, saving CU on instructions that touch few accounts.
82///
83/// ```ignore
84/// hopper_dispatch_lazy! {
85///     ctx;
86///     0 => process_init,
87///     1 => process_deposit,
88///     2 => process_withdraw,
89/// }
90/// ```
91#[macro_export]
92macro_rules! hopper_dispatch_lazy {
93    (
94        $ctx:expr;
95        $( $tag:literal => $handler:expr ),+ $(,)?
96    ) => {{
97        let data = $ctx.instruction_data();
98        // Event CPI passthrough.
99        if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xFE {
100            return Ok(());
101        }
102        if data.is_empty() {
103            return Err($crate::__runtime::error::ProgramError::InvalidInstructionData);
104        }
105        let tag = data[0];
106        match tag {
107            $( $tag => $handler($ctx), )+
108            _ => Err($crate::__runtime::error::ProgramError::InvalidInstructionData),
109        }
110    }};
111}
112
113/// 8-byte discriminator dispatch (Anchor/Quasar compatible).
114///
115/// Uses 8-byte discriminators instead of 1-byte tags. This allows
116/// interoperability with Anchor IDLs and Quasar programs.
117///
118/// ```ignore
119/// hopper_dispatch_8! {
120///     program_id, accounts, instruction_data;
121///     [0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d] => initialize,
122///     [0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6] => deposit,
123/// }
124/// ```
125#[macro_export]
126macro_rules! hopper_dispatch_8 {
127    (
128        $program_id:expr, $accounts:expr, $data:expr;
129        $( [ $($disc:literal),+ ] => $handler:expr ),+ $(,)?
130    ) => {{
131        // Event CPI passthrough.
132        if $data.len() >= 2 && $data[0] == 0xFF && $data[1] == 0xFE {
133            return Ok(());
134        }
135        let (disc, remaining) = $crate::dispatch::dispatch_instruction_8($data)?;
136        match disc {
137            $( [ $($disc),+ ] => $handler($program_id, $accounts, remaining), )+
138            _ => Err($crate::__runtime::error::ProgramError::InvalidInstructionData),
139        }
140    }};
141}