Skip to main content

hopper_runtime/
cpi_event.rs

1//! Self-CPI event emission primitives.
2//!
3//! Log output is lossy. Transaction metadata is not. A program that
4//! needs events to arrive at indexers regardless of log truncation
5//! invokes itself with a distinctive CPI whose bytes carry the event
6//! payload. This module provides the building blocks: a reserved
7//! discriminator so the dispatcher can route the CPI to a no-op
8//! sentinel, a wire-format helper for the instruction data, and a
9//! one-line pattern programs can copy.
10//!
11//! ## Wire format
12//!
13//! ```text
14//! [0..2]   CPI_EVENT_MARKER   (0xE0, 0x1E)
15//! [2]      event tag          (the byte from `#[hopper::event(tag = N)]`)
16//! [3..]    event payload      (from `HopperEvent::as_bytes()`)
17//! ```
18//!
19//! The two-byte marker is the reserved Hopper discriminator for
20//! self-CPI events and is unlikely to collide with any sensibly
21//! chosen user discriminator. The user-facing instruction
22//! declaration for the sentinel is:
23//!
24//! ```ignore
25//! #[instruction(discriminator = [0xE0, 0x1E])]
26//! fn __hopper_event_sink(_ctx: &mut Context<'_>) -> ProgramResult {
27//!     Ok(())
28//! }
29//! ```
30//!
31//! ## Why this pattern works
32//!
33//! Anchor's `emit_cpi!` uses the same trick: a self-CPI carrying
34//! payload bytes guarantees the event appears in the transaction's
35//! inner-instruction list, which RPC nodes do not truncate. Indexers
36//! scan for the reserved marker and decode the tail as the event.
37//!
38//! Hopper's version is leaner: a two-byte marker plus a one-byte
39//! event tag gives the indexer everything it needs to route without
40//! the Anchor eight-byte discriminator overhead.
41
42/// The reserved self-CPI event discriminator.
43///
44/// Placed at the start of every `emit_event_cpi` instruction and
45/// must be matched by a sentinel `#[instruction(discriminator = [0xE0, 0x1E])]`
46/// no-op handler in the calling program.
47pub const CPI_EVENT_MARKER: [u8; 2] = [0xE0, 0x1E];
48
49/// Canonical PDA seed for the Hopper event-authority. Match this in
50/// the program's sentinel handler setup so the CPI signer resolves.
51pub const EVENT_AUTHORITY_SEED: &[u8] = b"__hopper_event_authority";
52
53/// Fill an out buffer with the CPI wire format for an event.
54///
55/// Returns the number of bytes written. Caller picks the buffer size;
56/// `2 + 1 + E::PACKED_SIZE` is always sufficient. Returns `None` if
57/// the out buffer is too small.
58///
59/// Zero-alloc. Compiles to a pair of `copy_from_slice` calls.
60///
61/// ```ignore
62/// let mut buf = [0u8; 2 + 1 + Deposited::PACKED_SIZE];
63/// let len = hopper_runtime::cpi_event::encode_event_cpi(
64///     Deposited::TAG,
65///     event.as_bytes(),
66///     &mut buf,
67/// ).unwrap();
68///
69/// // Build an InstructionView and invoke_signed from here. The
70/// // sentinel handler accepts the CPI and returns Ok(()).
71/// ```
72#[inline]
73pub fn encode_event_cpi(
74    event_tag: u8,
75    event_payload: &[u8],
76    out: &mut [u8],
77) -> Option<usize> {
78    let total = 2 + 1 + event_payload.len();
79    if out.len() < total {
80        return None;
81    }
82    out[0..2].copy_from_slice(&CPI_EVENT_MARKER);
83    out[2] = event_tag;
84    out[3..total].copy_from_slice(event_payload);
85    Some(total)
86}
87
88/// Invoke a self-CPI carrying the encoded event payload.
89///
90/// Builds the one-account instruction (event-authority as signer) and
91/// hands it to the active backend's `invoke_signed`. The native
92/// backend path is the load-bearing one; a legacy-pinocchio-compat or
93/// solana-program-backend build routes through their respective
94/// compat shims.
95///
96/// This is the function [`crate::hopper_emit_cpi!`] calls. Users who
97/// want finer-grained control over the CPI (extra accounts, custom
98/// signer) can call this directly with their own encoded data.
99#[inline]
100pub fn invoke_event_cpi(
101    program_id: &crate::address::Address,
102    event_authority: &crate::account::AccountView,
103    data: &[u8],
104    authority_seeds: &[&[u8]],
105) -> crate::result::ProgramResult {
106    #[cfg(all(target_os = "solana", feature = "hopper-native-backend"))]
107    {
108        use crate::instruction::{InstructionAccount, InstructionView};
109        let account_meta = InstructionAccount {
110            pubkey: event_authority.address(),
111            is_signer: true,
112            is_writable: false,
113        };
114        let ix = InstructionView {
115            program_id,
116            accounts: ::core::slice::from_ref(&account_meta),
117            data,
118        };
119        // Array-of-slices form the native CPI surface expects for
120        // signer seeds: one signer, one seed list.
121        let signer_list = [authority_seeds];
122        let account_views = [event_authority];
123        crate::cpi::invoke_signed::<1>(&ix, &account_views, &signer_list[..])
124    }
125
126    #[cfg(any(
127        not(target_os = "solana"),
128        feature = "legacy-pinocchio-compat",
129        feature = "solana-program-backend",
130    ))]
131    {
132        let _ = (program_id, event_authority, data, authority_seeds);
133        // Off-chain or under a non-native backend: the self-CPI path
134        // is a no-op so host-side tests do not balloon into a CPI
135        // stub. Returning Ok keeps the handler happy; tests should
136        // assert on the encoded bytes via encode_event_cpi instead.
137        Ok(())
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn encodes_marker_tag_and_payload_in_order() {
147        let mut buf = [0u8; 16];
148        let len = encode_event_cpi(0x42, &[1, 2, 3, 4], &mut buf).unwrap();
149        assert_eq!(len, 7);
150        assert_eq!(&buf[..len], &[0xE0, 0x1E, 0x42, 1, 2, 3, 4]);
151    }
152
153    #[test]
154    fn rejects_short_buffer() {
155        let mut buf = [0u8; 3];
156        let len = encode_event_cpi(0, &[1, 2, 3, 4], &mut buf);
157        assert!(len.is_none());
158    }
159
160    #[test]
161    fn zero_payload_is_valid() {
162        let mut buf = [0u8; 3];
163        let len = encode_event_cpi(0x7F, &[], &mut buf).unwrap();
164        assert_eq!(len, 3);
165        assert_eq!(&buf[..len], &[0xE0, 0x1E, 0x7F]);
166    }
167
168    #[test]
169    fn reserved_marker_is_stable() {
170        assert_eq!(CPI_EVENT_MARKER, [0xE0, 0x1E]);
171    }
172}