Skip to main content

hopper/
receipts.rs

1//! State receipts and event emission.
2//!
3//! Receipts are Hopper's way of making state changes observable.
4//! After mutating state, programs emit a receipt that can be:
5//! - logged to the runtime for indexers/explorers
6//! - returned as instruction data for CPI callers
7//! - used by testing and auditing tools
8//!
9//! This is Hopper's current state-observability surface.
10//! Raw receipts, tagged receipts, typed receipts, and CPI return data
11//! are all available today, and the same wire formats feed schema and
12//! manager tooling.
13
14use hopper_runtime::ProgramResult;
15
16/// A byte slice descriptor matching the Solana runtime's `SolBytes` ABI.
17#[cfg(target_os = "solana")]
18#[repr(C)]
19struct SolBytes {
20    ptr: *const u8,
21    len: u64,
22}
23
24#[cfg(target_os = "solana")]
25extern "C" {
26    fn sol_log_data(data: *const SolBytes, data_len: u64);
27    fn sol_set_return_data(data: *const u8, length: u64);
28}
29
30/// Emit a raw receipt (log the bytes to the runtime).
31///
32/// Uses `sol_log_data` on BPF, which emits structured binary data
33/// that indexers and explorers can parse.
34///
35/// # Example
36///
37/// ```ignore
38/// let data = amount.to_le_bytes();
39/// emit_receipt(&data)?;
40/// ```
41#[inline(always)]
42pub fn emit_receipt(data: &[u8]) -> ProgramResult {
43    #[cfg(target_os = "solana")]
44    {
45        let field = SolBytes {
46            ptr: data.as_ptr(),
47            len: data.len() as u64,
48        };
49        unsafe {
50            sol_log_data(&field as *const SolBytes, 1);
51        }
52    }
53    #[cfg(not(target_os = "solana"))]
54    {
55        let _ = data;
56    }
57    Ok(())
58}
59
60/// Emit a receipt with a type tag prefix.
61///
62/// Prepends a single byte tag before the data, allowing indexers to
63/// distinguish different receipt types from the same program.
64#[inline(always)]
65pub fn emit_tagged_receipt(tag: u8, data: &[u8]) -> ProgramResult {
66    #[cfg(target_os = "solana")]
67    {
68        let tag_byte: [u8; 1] = [tag];
69        let fields: [SolBytes; 2] = [
70            SolBytes {
71                ptr: tag_byte.as_ptr(),
72                len: 1,
73            },
74            SolBytes {
75                ptr: data.as_ptr(),
76                len: data.len() as u64,
77            },
78        ];
79        unsafe {
80            sol_log_data(fields.as_ptr(), 2);
81        }
82    }
83    #[cfg(not(target_os = "solana"))]
84    {
85        let _ = (tag, data);
86    }
87    Ok(())
88}
89
90/// Set return data for the current instruction.
91///
92/// Makes data available to the calling program via `get_return_data()`.
93/// Maximum 1024 bytes per Solana runtime limits.
94#[inline(always)]
95pub fn set_return_data(data: &[u8]) -> ProgramResult {
96    #[cfg(target_os = "solana")]
97    {
98        unsafe {
99            sol_set_return_data(data.as_ptr(), data.len() as u64);
100        }
101    }
102    #[cfg(not(target_os = "solana"))]
103    {
104        let _ = data;
105    }
106    Ok(())
107}
108
109/// Trait for types that can be emitted as receipts.
110///
111/// Implement this for your domain-specific receipt structs to get
112/// typed emission via `emit_typed_receipt()`.
113pub trait Receipt {
114    /// Receipt type tag for indexer discrimination.
115    const TAG: u8;
116
117    /// Serialize the receipt to bytes.
118    ///
119    /// The returned slice must be valid for the lifetime of `self`.
120    fn as_bytes(&self) -> &[u8];
121}
122
123/// Emit a typed receipt.
124///
125/// Automatically prepends the type tag from `Receipt::TAG`.
126#[inline(always)]
127pub fn emit_typed_receipt<T: Receipt>(receipt: &T) -> ProgramResult {
128    emit_tagged_receipt(T::TAG, receipt.as_bytes())
129}