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