Skip to main content

hopper_native/
return_data.rs

1//! CPI return data retrieval and typed deserialization.
2//!
3//! The Solana runtime supports return data from CPI calls (up to 1024 bytes).
4//! No framework provides a typed wrapper that combines invoke + deserialize
5//! in one step. Hopper does.
6
7use crate::address::Address;
8use crate::error::ProgramError;
9use crate::project::Projectable;
10
11#[cfg(feature = "cpi")]
12use crate::instruction::{InstructionView, Signer};
13
14/// Maximum return data size (1 KiB), matching Solana runtime limit.
15pub const MAX_RETURN_DATA: usize = 1024;
16
17/// Return data from a previous CPI call.
18pub struct ReturnData {
19    /// Buffer holding the return data (stack-allocated).
20    buf: [u8; MAX_RETURN_DATA],
21    /// Actual length of the return data.
22    len: usize,
23    /// Program ID that set the return data.
24    program_id: Address,
25}
26
27impl ReturnData {
28    /// Get the return data bytes.
29    #[inline(always)]
30    pub fn data(&self) -> &[u8] {
31        &self.buf[..self.len]
32    }
33
34    /// Get the program that set the return data.
35    #[inline(always)]
36    pub fn program_id(&self) -> &Address {
37        &self.program_id
38    }
39
40    /// Length of the return data.
41    #[inline(always)]
42    pub fn len(&self) -> usize {
43        self.len
44    }
45
46    /// Whether the return data is empty.
47    #[inline(always)]
48    pub fn is_empty(&self) -> bool {
49        self.len == 0
50    }
51
52    /// Interpret the return data as a `Projectable` type.
53    ///
54    /// Returns `Err(AccountDataTooSmall)` if the return data is smaller
55    /// than `size_of::<T>()`.
56    #[inline]
57    pub fn as_type<T: Projectable>(&self) -> Result<&T, ProgramError> {
58        let size = core::mem::size_of::<T>();
59        if self.len < size {
60            return Err(ProgramError::AccountDataTooSmall);
61        }
62
63        let align = core::mem::align_of::<T>();
64        let ptr = self.buf.as_ptr();
65        if align > 1 && (ptr as usize) % align != 0 {
66            return Err(ProgramError::InvalidAccountData);
67        }
68
69        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
70        Ok(unsafe { &*(ptr as *const T) })
71    }
72
73    /// Read a u64 from the first 8 bytes of return data.
74    #[inline]
75    pub fn as_u64(&self) -> Result<u64, ProgramError> {
76        if self.len < 8 {
77            return Err(ProgramError::AccountDataTooSmall);
78        }
79        let mut bytes = [0u8; 8];
80        bytes.copy_from_slice(&self.buf[..8]);
81        Ok(u64::from_le_bytes(bytes))
82    }
83
84    /// Read a u32 from the first 4 bytes of return data.
85    #[inline]
86    pub fn as_u32(&self) -> Result<u32, ProgramError> {
87        if self.len < 4 {
88            return Err(ProgramError::AccountDataTooSmall);
89        }
90        let mut bytes = [0u8; 4];
91        bytes.copy_from_slice(&self.buf[..4]);
92        Ok(u32::from_le_bytes(bytes))
93    }
94}
95
96/// Retrieve return data from the most recent CPI call.
97///
98/// Returns `None` if no return data was set (length == 0).
99#[inline]
100pub fn get_return_data() -> Option<ReturnData> {
101    #[allow(unused_mut)]
102    let mut rd = ReturnData {
103        buf: [0u8; MAX_RETURN_DATA],
104        len: 0,
105        program_id: Address::default(),
106    };
107
108    #[cfg(target_os = "solana")]
109    {
110        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
111        let actual_len = unsafe {
112            crate::syscalls::sol_get_return_data(
113                rd.buf.as_mut_ptr(),
114                MAX_RETURN_DATA as u64,
115                rd.program_id.0.as_mut_ptr(),
116            )
117        };
118        rd.len = (actual_len as usize).min(MAX_RETURN_DATA);
119    }
120
121    #[cfg(not(target_os = "solana"))]
122    {
123        // Off-chain: no return data available.
124    }
125
126    if rd.len == 0 {
127        None
128    } else {
129        Some(rd)
130    }
131}
132
133/// Invoke a CPI and immediately read back typed return data.
134///
135/// Combines `invoke_signed` + `get_return_data` + `as_type::<T>()` into
136/// a single operation. This is the cleanest way to call a program that
137/// returns structured data.
138///
139/// # Example
140///
141/// ```ignore
142/// let oracle_price: &PriceData = invoke_and_read::<PriceData, 2>(
143///     &instruction,
144///     &[&oracle_program, &price_feed],
145///     &[],
146/// )?;
147/// ```
148#[cfg(feature = "cpi")]
149#[inline]
150pub fn invoke_and_read<'a, T: Projectable, const ACCOUNTS: usize>(
151    instruction: &InstructionView,
152    account_views: &[&crate::account_view::AccountView; ACCOUNTS],
153    signers_seeds: &[Signer],
154) -> Result<ReturnData, ProgramError> {
155    crate::cpi::invoke_signed::<ACCOUNTS>(instruction, account_views, signers_seeds)?;
156
157    get_return_data().ok_or(ProgramError::InvalidAccountData)
158}