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}