Skip to main content

hopper_runtime/
lazy.rs

1//! Runtime-typed lazy account parsing.
2//!
3//! The substrate's [`hopper_native::LazyContext`] defers account parsing until
4//! a handler requests an account. This module wraps that context with Hopper
5//! runtime [`AccountView`], [`Address`], and [`ProgramError`] types. The
6//! wrapper delegates parsing to the substrate;
7//! it does not copy account data.
8//!
9//! ```ignore
10//! hopper::lazy_entrypoint!(process);
11//!
12//! fn process(ctx: &mut hopper::prelude::LazyContext) -> ProgramResult {
13//!     let payer = ctx.next_signer()?;          // runtime AccountView
14//!     let vault = ctx.next_writable()?;
15//!     match ctx.instruction_data().first() {
16//!         Some(0) => ping(),
17//!         _ => Err(ProgramError::InvalidInstructionData),
18//!     }
19//! }
20//! ```
21//!
22//! Programs that want native substrate types can use the substrate-level
23//! `hopper_lazy_entrypoint!` macro directly.
24
25use crate::account::AccountView;
26use crate::address::Address;
27use crate::error::ProgramError;
28
29/// Runtime-typed view over a native lazy parsing context.
30///
31/// Construction happens inside the runtime's `hopper_lazy_entrypoint!`
32/// expansion ([`LazyContext::from_native`]); handlers only ever see this
33/// wrapper. Every method delegates to the substrate implementation and
34/// converts at the boundary without re-parsing or copying account data.
35pub struct LazyContext<'a, 'info> {
36    inner: &'a mut hopper_native::LazyContext<'info>,
37}
38
39impl<'a, 'info> LazyContext<'a, 'info> {
40    /// Wrap a substrate lazy context. Called by the runtime's
41    /// `hopper_lazy_entrypoint!` expansion; public so custom entrypoint
42    /// plumbing can bridge the same way.
43    #[inline(always)]
44    pub fn from_native(inner: &'a mut hopper_native::LazyContext<'info>) -> Self {
45        Self { inner }
46    }
47
48    /// Instruction data for this invocation. Available at any time,
49    /// including before any account is consumed.
50    #[inline(always)]
51    pub fn instruction_data(&self) -> &[u8] {
52        self.inner.instruction_data()
53    }
54
55    /// The executing program's id.
56    #[inline(always)]
57    pub fn program_id(&self) -> &Address {
58        let native: &hopper_native::Address = self.inner.program_id();
59        // SAFETY: `Address` is a transparent 32-byte wrapper shared by
60        // the native and runtime layers; the reinterpret is
61        // layout-identical (same cast the eager entrypoint macro makes).
62        unsafe { &*(native as *const hopper_native::Address as *const Address) }
63    }
64
65    /// Total accounts the instruction declared.
66    #[inline(always)]
67    pub fn total_accounts(&self) -> usize {
68        self.inner.total_accounts()
69    }
70
71    /// How many accounts have been parsed so far.
72    #[inline(always)]
73    pub fn parsed_count(&self) -> usize {
74        self.inner.parsed_count()
75    }
76
77    /// How many declared accounts remain unparsed.
78    #[inline(always)]
79    pub fn remaining(&self) -> usize {
80        self.inner.remaining()
81    }
82
83    /// Parse and return the next account.
84    #[inline(always)]
85    pub fn next_account(&mut self) -> Result<AccountView<'info>, ProgramError> {
86        self.inner
87            .next_account()
88            .map(AccountView::from_inner)
89            .map_err(ProgramError::from)
90    }
91
92    /// Parse the next account and require it to be a signer.
93    #[inline(always)]
94    pub fn next_signer(&mut self) -> Result<AccountView<'info>, ProgramError> {
95        self.inner
96            .next_signer()
97            .map(AccountView::from_inner)
98            .map_err(ProgramError::from)
99    }
100
101    /// Parse the next account and require it to be writable.
102    #[inline(always)]
103    pub fn next_writable(&mut self) -> Result<AccountView<'info>, ProgramError> {
104        self.inner
105            .next_writable()
106            .map(AccountView::from_inner)
107            .map_err(ProgramError::from)
108    }
109
110    /// Parse the next account and require signer + writable (a fee
111    /// payer shape).
112    #[inline(always)]
113    pub fn next_payer(&mut self) -> Result<AccountView<'info>, ProgramError> {
114        self.inner
115            .next_payer()
116            .map(AccountView::from_inner)
117            .map_err(ProgramError::from)
118    }
119
120    /// Parse the next account and require its owner to be `program`.
121    #[inline(always)]
122    pub fn next_owned_by(&mut self, program: &Address) -> Result<AccountView<'info>, ProgramError> {
123        // SAFETY: transparent 32-byte address reinterpret, as above.
124        let native = unsafe { &*(program as *const Address as *const hopper_native::Address) };
125        self.inner
126            .next_owned_by(native)
127            .map(AccountView::from_inner)
128            .map_err(ProgramError::from)
129    }
130
131    /// Skip the next `n` accounts without exposing them.
132    #[inline(always)]
133    pub fn skip(&mut self, n: usize) -> Result<(), ProgramError> {
134        self.inner.skip(n).map_err(ProgramError::from)
135    }
136
137    /// Parse every remaining account and return them as a slice.
138    #[inline(always)]
139    pub fn drain_remaining(&mut self) -> Result<&[AccountView<'info>], ProgramError> {
140        match self.inner.drain_remaining() {
141            Ok(native) => {
142                // SAFETY: runtime `AccountView` is `repr(transparent)`
143                // over the native view (the eager entrypoint macro makes
144                // the identical whole-slice reinterpret).
145                let views = unsafe {
146                    core::slice::from_raw_parts(
147                        native.as_ptr() as *const AccountView<'info>,
148                        native.len(),
149                    )
150                };
151                Ok(views)
152            }
153            Err(e) => Err(ProgramError::from(e)),
154        }
155    }
156
157    /// Already-parsed account at `index`, if it has been consumed.
158    #[inline(always)]
159    pub fn get(&self, index: usize) -> Option<&AccountView<'info>> {
160        self.inner.get(index).map(|native| {
161            // SAFETY: transparent single-view reinterpret, as above.
162            unsafe {
163                &*(native as *const hopper_native::AccountView<'info> as *const AccountView<'info>)
164            }
165        })
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    extern crate std;
172    use super::*;
173
174    /// Build a minimal loader input frame: `count` fresh accounts of
175    /// `data_len` bytes each (first one a signer), then instruction
176    /// data, then a program id, the same layout the native lazy tests
177    /// construct, reduced to what these bridge tests need.
178    fn build_frame(
179        count: u64,
180        data_len: usize,
181        ix_data: &[u8],
182        program_id: [u8; 32],
183    ) -> std::vec::Vec<u8> {
184        use hopper_native::RuntimeAccount;
185        let mut buf: std::vec::Vec<u8> = std::vec::Vec::new();
186        buf.extend_from_slice(&count.to_le_bytes());
187        for i in 0..count {
188            let start = buf.len();
189            // Marker byte 0xFF (fresh) leads the RuntimeAccount header.
190            let mut header = [0u8; core::mem::size_of::<RuntimeAccount>()];
191            header[0] = 0xFF;
192            // SAFETY: header is sized exactly for RuntimeAccount; we
193            // construct the value then copy its bytes (test-only).
194            let acct = RuntimeAccount {
195                borrow_state: 0xFF,
196                is_signer: (i == 0) as u8,
197                is_writable: 1,
198                executable: 0,
199                resize_delta: 0,
200                address: hopper_native::Address::new_from_array([i as u8 + 1; 32]),
201                owner: hopper_native::Address::new_from_array([9; 32]),
202                lamports: 5,
203                data_len: data_len as u64,
204            };
205            // SAFETY: plain-data struct viewed as bytes, test-only.
206            let bytes = unsafe {
207                core::slice::from_raw_parts(
208                    &acct as *const RuntimeAccount as *const u8,
209                    core::mem::size_of::<RuntimeAccount>(),
210                )
211            };
212            header.copy_from_slice(bytes);
213            header[0] = 0xFF;
214            buf.extend_from_slice(&header);
215            buf.extend_from_slice(&std::vec![0u8; data_len]);
216            // 10 KiB realloc reserve + pad to 8.
217            buf.extend_from_slice(&std::vec![0u8; 10 * 1024]);
218            let advanced = buf.len() - start;
219            let pad = (8 - (advanced % 8)) % 8;
220            buf.extend_from_slice(&std::vec![0u8; pad]);
221            // Rent epoch.
222            buf.extend_from_slice(&u64::MAX.to_le_bytes());
223        }
224        buf.extend_from_slice(&(ix_data.len() as u64).to_le_bytes());
225        buf.extend_from_slice(ix_data);
226        buf.extend_from_slice(&program_id);
227        buf
228    }
229
230    #[test]
231    fn bridge_yields_runtime_types_end_to_end() {
232        let mut frame = build_frame(2, 8, &[7, 1, 2], [42; 32]);
233        // SAFETY: `frame` is a well-formed loader input buffer built
234        // above and outlives the context (held for the whole test).
235        let mut native = unsafe { hopper_native::lazy::lazy_deserialize(frame.as_mut_ptr()) };
236        let mut ctx = LazyContext::from_native(&mut native);
237
238        assert_eq!(ctx.total_accounts(), 2);
239        assert_eq!(ctx.remaining(), 2);
240        assert_eq!(ctx.instruction_data(), &[7, 1, 2]);
241        assert_eq!(ctx.program_id(), &Address::new([42u8; 32]));
242
243        // Runtime-typed views come back with the right identities.
244        let first = ctx.next_signer().expect("first is a signer");
245        assert_eq!(first.address(), &Address::new([1u8; 32]));
246        assert!(first.is_signer());
247        let second = ctx.next_account().expect("second parses");
248        assert_eq!(second.address(), &Address::new([2u8; 32]));
249        assert_eq!(ctx.parsed_count(), 2);
250        assert_eq!(ctx.remaining(), 0);
251
252        // get() re-exposes parsed views by index, runtime-typed.
253        assert_eq!(ctx.get(0).unwrap().address(), &Address::new([1u8; 32]));
254        assert!(ctx.get(2).is_none());
255
256        // Errors arrive as RUNTIME ProgramError variants.
257        assert_eq!(
258            ctx.next_account().unwrap_err(),
259            ProgramError::NotEnoughAccountKeys,
260            "exhaustion maps through the layout-twin glue"
261        );
262    }
263
264    #[test]
265    fn signer_requirement_maps_to_the_runtime_error() {
266        let mut frame = build_frame(2, 8, &[0], [1; 32]);
267        // SAFETY: as above, well-formed frame, outlives the context.
268        let mut native = unsafe { hopper_native::lazy::lazy_deserialize(frame.as_mut_ptr()) };
269        let mut ctx = LazyContext::from_native(&mut native);
270        ctx.skip(1).expect("skip the signer");
271        assert_eq!(
272            ctx.next_signer().unwrap_err(),
273            ProgramError::MissingRequiredSignature
274        );
275    }
276
277    #[test]
278    fn drain_remaining_casts_the_whole_slice() {
279        let mut frame = build_frame(3, 4, &[0], [1; 32]);
280        // SAFETY: as above.
281        let mut native = unsafe { hopper_native::lazy::lazy_deserialize(frame.as_mut_ptr()) };
282        let mut ctx = LazyContext::from_native(&mut native);
283        let _first = ctx.next_account().unwrap();
284        let rest = ctx.drain_remaining().expect("drains");
285        assert_eq!(rest.len(), 2);
286        assert_eq!(rest[0].address(), &Address::new([2u8; 32]));
287        assert_eq!(rest[1].address(), &Address::new([3u8; 32]));
288    }
289}