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//! This module combines invocation, program-id validation, and typed return-data
5//! decoding.
6
7use crate::address::Address;
8use crate::error::ProgramError;
9use crate::project::Projectable;
10use core::mem::MaybeUninit;
11
12#[cfg(feature = "cpi")]
13use crate::instruction::{InstructionView, Signer};
14
15/// Maximum return data size (1 KiB), matching Solana runtime limit.
16pub const MAX_RETURN_DATA: usize = 1024;
17
18/// Return data from a previous CPI call.
19///
20/// The buffer is deliberately left uninitialized until the
21/// `sol_get_return_data` syscall fills it; only the syscall-initialized
22/// prefix (`len` bytes) is ever exposed to callers.
23pub struct ReturnData {
24    /// Buffer holding the return data (stack-allocated; only the first
25    /// `len` bytes are initialized).
26    buf: [MaybeUninit<u8>; MAX_RETURN_DATA],
27    /// Actual length of the return data.
28    len: usize,
29    /// Program ID that set the return data.
30    program_id: Address,
31}
32
33impl ReturnData {
34    /// Get the return data bytes.
35    #[inline(always)]
36    pub fn data(&self) -> &[u8] {
37        // Fail-closed backstop for the invariant the SAFETY comment relies
38        // on: `len` can never exceed the buffer capacity.
39        debug_assert!(self.len <= MAX_RETURN_DATA);
40        // SAFETY: `sol_get_return_data` initializes exactly
41        // `min(actual_len, MAX_RETURN_DATA)` bytes of the buffer it was
42        // handed, and `get_return_data` sets `len` to that same value (the
43        // test constructor likewise writes `len` bytes before setting it), so
44        // the first `len` bytes are always initialized `u8`s.
45        unsafe { core::slice::from_raw_parts(self.buf.as_ptr() as *const u8, self.len) }
46    }
47
48    /// Get the program that set the return data.
49    #[inline(always)]
50    pub fn program_id(&self) -> &Address {
51        &self.program_id
52    }
53
54    /// Length of the return data.
55    #[inline(always)]
56    pub fn len(&self) -> usize {
57        self.len
58    }
59
60    /// Whether the return data is empty.
61    #[inline(always)]
62    pub fn is_empty(&self) -> bool {
63        self.len == 0
64    }
65
66    /// Interpret the return data as a `Projectable` type.
67    ///
68    /// Returns `Err(AccountDataTooSmall)` if the return data is smaller
69    /// than `size_of::<T>()`.
70    #[inline]
71    pub fn as_type<T: Projectable>(&self) -> Result<&T, ProgramError> {
72        let size = core::mem::size_of::<T>();
73        if self.len < size {
74            return Err(ProgramError::AccountDataTooSmall);
75        }
76
77        let data = self.data();
78        let align = core::mem::align_of::<T>();
79        let ptr = data.as_ptr();
80        if !(ptr as usize).is_multiple_of(align) {
81            return Err(ProgramError::InvalidAccountData);
82        }
83
84        // SAFETY: `data` is the initialized `len`-byte prefix of the buffer,
85        // the length check above guarantees `len >= size_of::<T>()`, the
86        // alignment check guarantees `ptr` is aligned for `T`, and
87        // `T: Projectable` is valid for any initialized bit pattern.
88        Ok(unsafe { &*(ptr as *const T) })
89    }
90
91    /// Read a typed prefix only when the expected program produced this data.
92    /// Nested CPIs can leave a different producer's return data behind.
93    /// The application remains responsible for the payload's business rules.
94    #[inline]
95    pub fn as_type_from<T: Projectable>(
96        &self,
97        expected_program: &Address,
98    ) -> Result<&T, ProgramError> {
99        if !crate::address::address_eq(self.program_id(), expected_program) {
100            return Err(ProgramError::IncorrectProgramId);
101        }
102        self.as_type::<T>()
103    }
104
105    /// Read a u64 from the first 8 bytes of return data.
106    #[inline]
107    pub fn as_u64(&self) -> Result<u64, ProgramError> {
108        if self.len < 8 {
109            return Err(ProgramError::AccountDataTooSmall);
110        }
111        let mut bytes = [0u8; 8];
112        bytes.copy_from_slice(&self.data()[..8]);
113        Ok(u64::from_le_bytes(bytes))
114    }
115
116    /// Read a u32 from the first 4 bytes of return data.
117    #[inline]
118    pub fn as_u32(&self) -> Result<u32, ProgramError> {
119        if self.len < 4 {
120            return Err(ProgramError::AccountDataTooSmall);
121        }
122        let mut bytes = [0u8; 4];
123        bytes.copy_from_slice(&self.data()[..4]);
124        Ok(u32::from_le_bytes(bytes))
125    }
126}
127
128/// Retrieve return data from the most recent CPI call.
129///
130/// Returns `None` if no return data was set (length == 0).
131///
132/// Only the initialized prefix reported by the syscall is exposed. Empty
133/// return data yields `None`; accessors never read the uninitialized remainder.
134/// This raw snapshot does not authenticate a producer. Use `as_type_from` or
135/// `invoke_and_read` when interpreting a particular program's result.
136#[inline]
137pub fn get_return_data() -> Option<ReturnData> {
138    #[allow(unused_mut)]
139    let mut rd = ReturnData {
140        buf: [const { MaybeUninit::uninit() }; MAX_RETURN_DATA],
141        len: 0,
142        program_id: Address::default(),
143    };
144
145    #[cfg(target_os = "solana")]
146    {
147        // SAFETY: The buffer and program-id pointers are stack-allocated with
148        // the exact capacities advertised to the runtime syscall; the buffer
149        // may be uninitialized because the syscall only writes (never reads)
150        // it.
151        let actual_len = unsafe {
152            crate::syscalls::sol_get_return_data(
153                rd.buf.as_mut_ptr() as *mut u8,
154                MAX_RETURN_DATA as u64,
155                rd.program_id.0.as_mut_ptr(),
156            )
157        };
158        rd.len = (actual_len as usize).min(MAX_RETURN_DATA);
159    }
160
161    #[cfg(not(target_os = "solana"))]
162    {
163        // Off-chain: no return data available; `len` stays 0 so the
164        // uninitialized buffer is discarded below without being read.
165    }
166
167    if rd.len == 0 {
168        None
169    } else {
170        Some(rd)
171    }
172}
173
174/// Invoke a CPI and capture a producer-checked, type-validated return snapshot.
175///
176/// The producing program must equal `instruction.program_id`. The snapshot
177/// must contain an aligned `T` prefix; trailing bytes are permitted, matching
178/// `ReturnData::as_type`. No data is `InvalidAccountData`, a different producer
179/// is `IncorrectProgramId`, and a short result is `AccountDataTooSmall`.
180/// Call `as_type::<T>()` on the returned snapshot to borrow the value.
181///
182/// ```ignore
183/// let snapshot = invoke_and_read::<PriceData, 2>(&instruction, &accounts, &[])?;
184/// let oracle_price = snapshot.as_type::<PriceData>()?;
185/// ```
186#[cfg(feature = "cpi")]
187#[inline]
188pub fn invoke_and_read<T: Projectable, const ACCOUNTS: usize>(
189    instruction: &InstructionView<'_, '_, '_, '_>,
190    account_views: &[&crate::account_view::AccountView<'_>; ACCOUNTS],
191    signers_seeds: &[Signer<'_, '_>],
192) -> Result<ReturnData, ProgramError> {
193    crate::cpi::invoke_signed::<ACCOUNTS>(instruction, account_views, signers_seeds)?;
194
195    let returned = get_return_data().ok_or(ProgramError::InvalidAccountData)?;
196    returned.as_type_from::<T>(instruction.program_id)?;
197    Ok(returned)
198}
199
200#[cfg(test)]
201impl ReturnData {
202    /// Test-only constructor: builds a snapshot whose buffer prefix is fully
203    /// initialized from `bytes`, mirroring what the syscall produces on-chain.
204    fn test_snapshot(bytes: &[u8], program_id: Address) -> Self {
205        assert!(bytes.len() <= MAX_RETURN_DATA);
206        let mut buf = [const { MaybeUninit::uninit() }; MAX_RETURN_DATA];
207        for (dst, src) in buf.iter_mut().zip(bytes) {
208            dst.write(*src);
209        }
210        ReturnData {
211            buf,
212            len: bytes.len(),
213            program_id,
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn typed_return_requires_the_expected_producer_and_initialized_type() {
224        let expected = Address::new_from_array([1; 32]);
225        let nested = Address::new_from_array([2; 32]);
226        let correct = ReturnData::test_snapshot(&42u64.to_le_bytes(), expected.clone());
227        assert_eq!(*correct.as_type_from::<u64>(&expected).unwrap(), 42);
228        assert_eq!(
229            correct.as_type_from::<u64>(&nested),
230            Err(ProgramError::IncorrectProgramId)
231        );
232        let short = ReturnData::test_snapshot(&[42], expected.clone());
233        assert_eq!(
234            short.as_type_from::<u64>(&expected),
235            Err(ProgramError::AccountDataTooSmall)
236        );
237        let forwarded = ReturnData::test_snapshot(&42u64.to_le_bytes(), nested);
238        assert_eq!(
239            forwarded.as_type_from::<u64>(&expected),
240            Err(ProgramError::IncorrectProgramId)
241        );
242    }
243
244    #[test]
245    fn offchain_get_return_data_is_none() {
246        assert!(get_return_data().is_none());
247    }
248
249    #[test]
250    fn data_exposes_exactly_the_written_prefix() {
251        let payload = [0xAB, 0xCD, 0xEF];
252        let rd = ReturnData::test_snapshot(&payload, Address::default());
253        assert_eq!(rd.data(), &payload);
254        assert_eq!(rd.len(), payload.len());
255        assert!(!rd.is_empty());
256    }
257
258    #[test]
259    fn as_u64_and_as_u32_never_read_past_the_prefix() {
260        let short = ReturnData::test_snapshot(&[1, 2, 3], Address::default());
261        assert!(short.as_u64().is_err());
262        assert!(short.as_u32().is_err());
263
264        let rd = ReturnData::test_snapshot(&7u64.to_le_bytes(), Address::default());
265        assert_eq!(rd.as_u64().unwrap(), 7);
266        assert_eq!(rd.as_u32().unwrap(), 7);
267    }
268
269    #[test]
270    fn as_type_length_checks_against_the_prefix() {
271        let rd = ReturnData::test_snapshot(&[5u8], Address::default());
272        assert!(rd.as_type::<u64>().is_err());
273        assert_eq!(*rd.as_type::<u8>().unwrap(), 5);
274    }
275}