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}