hopper_runtime/account_wrappers.rs
1//! Anchor-grade typed account wrappers for `#[hopper::context]`.
2//!
3//! Closes Hopper Safety Audit Stage 2.3: zero-cost, zero-alignment,
4//! type-directed wrappers that programs can use in context structs to
5//! name an account's *role* rather than paint it with an
6//! `#[account(signer)]` attribute.
7//!
8//! ```ignore
9//! #[hopper::context]
10//! pub struct Deposit<'info> {
11//! pub authority: Signer<'info>,
12//! pub vault: Account<'info, Vault>,
13//! pub system_program: Program<'info, SystemId>,
14//! }
15//! ```
16//!
17//! The context macro recognizes these type names via
18//! `skips_layout_validation` and auto-derives the appropriate
19//! checks (`check_signer`, `check_owned_by`, `check_executable`,
20//! address-pin). The wrappers themselves are
21//! `#[repr(transparent)]` over `&AccountView` so they compile away
22//! to the same pointer access as the raw form.
23//!
24//! # Why wrappers alongside the attribute path
25//!
26//! The attribute-directed lowering (`#[account(signer, mut)]`) and
27//! the wrapper-directed lowering (`pub authority: Signer<'info>`)
28//! both cover the same safety story. The wrapper form is
29//! Anchor-familiar and makes the role visible in every signature
30//! that accepts the account; the attribute form stays available for
31//! callers who prefer explicit constraint-lists. Both paths flow
32//! through the same canonical runtime checks. there is no
33//! duplicate safety implementation.
34
35use core::marker::PhantomData;
36
37use crate::account::AccountView;
38use crate::address::Address;
39
40/// Account that must be a transaction signer.
41///
42/// The `#[hopper::context]` macro treats a `Signer<'info>` field
43/// identically to `#[account(signer)] pub x: AccountView`. the
44/// emitted `validate_{field}()` calls `check_signer()`.
45#[repr(transparent)]
46#[derive(Clone, Copy)]
47pub struct Signer<'info> {
48 inner: &'info AccountView,
49}
50
51impl<'info> Signer<'info> {
52 /// Wrap an `AccountView` that has already been verified as a
53 /// signer. The macro-generated `validate_{field}()` call emits
54 /// the `check_signer` first, so by the time the wrapper is
55 /// constructed the invariant already holds.
56 #[inline(always)]
57 pub unsafe fn new_unchecked(view: &'info AccountView) -> Self {
58 Self { inner: view }
59 }
60
61 /// Wrap an `AccountView` after verifying the signer invariant.
62 /// Prefer the macro-emitted `validate_{field}()` path when the
63 /// account is part of a `#[hopper::context]` struct.
64 #[inline]
65 pub fn try_new(view: &'info AccountView) -> Result<Self, crate::error::ProgramError> {
66 view.check_signer()?;
67 Ok(Self { inner: view })
68 }
69
70 /// The underlying account view.
71 #[inline(always)]
72 pub fn as_account(&self) -> &'info AccountView {
73 self.inner
74 }
75
76 /// The signer's public key.
77 #[inline(always)]
78 pub fn key(&self) -> &Address {
79 self.inner.address()
80 }
81}
82
83impl<'info> core::ops::Deref for Signer<'info> {
84 type Target = AccountView;
85 #[inline(always)]
86 fn deref(&self) -> &AccountView {
87 self.inner
88 }
89}
90
91/// Account with a verified Hopper layout owned by the executing program.
92///
93/// `Account<'info, T>` expands to the same checks as
94/// `#[account]` with `layout = T`: `check_owned_by(program_id)` +
95/// `load::<T>()` (which verifies the header, discriminator, version,
96/// and wire fingerprint). Field access is through `get()` / `get_mut()`
97/// which return typed references into the borrowed account data.
98#[repr(transparent)]
99pub struct Account<'info, T: crate::layout::LayoutContract> {
100 inner: &'info AccountView,
101 _ty: PhantomData<T>,
102}
103
104impl<'info, T: crate::layout::LayoutContract> Clone for Account<'info, T> {
105 fn clone(&self) -> Self {
106 *self
107 }
108}
109impl<'info, T: crate::layout::LayoutContract> Copy for Account<'info, T> {}
110
111impl<'info, T: crate::layout::LayoutContract> Account<'info, T> {
112 /// Wrap an already-validated `AccountView`. Unsafe because the
113 /// caller must have verified owner + layout header.
114 #[inline(always)]
115 pub unsafe fn new_unchecked(view: &'info AccountView) -> Self {
116 Self {
117 inner: view,
118 _ty: PhantomData,
119 }
120 }
121
122 /// Wrap with owner + layout verification.
123 #[inline]
124 pub fn try_new(
125 view: &'info AccountView,
126 owner: &Address,
127 ) -> Result<Self, crate::error::ProgramError> {
128 view.check_owned_by(owner)?;
129 let _ = view.load::<T>()?;
130 Ok(Self {
131 inner: view,
132 _ty: PhantomData,
133 })
134 }
135
136 /// The underlying account view.
137 #[inline(always)]
138 pub fn as_account(&self) -> &'info AccountView {
139 self.inner
140 }
141
142 /// Borrow the typed layout for reading.
143 #[inline(always)]
144 pub fn load(&self) -> Result<crate::borrow::Ref<'_, T>, crate::error::ProgramError> {
145 self.inner.load::<T>()
146 }
147
148 /// Borrow the typed layout for writing.
149 #[inline(always)]
150 pub fn load_mut(&self) -> Result<crate::borrow::RefMut<'_, T>, crate::error::ProgramError> {
151 self.inner.load_mut::<T>()
152 }
153}
154
155/// Account that is expected to be *created* during this instruction.
156///
157/// `InitAccount<'info, T>` skips the layout-header check at validation
158/// time (there's nothing to validate yet. the CPI hasn't run) but
159/// otherwise behaves like `Account<'info, T>`. The `#[hopper::context]`
160/// macro pairs it with `#[account(init, payer = ..., space = ...)]`
161/// to emit the `init_{field}()` lifecycle helper that actually
162/// performs the System Program CPI.
163#[repr(transparent)]
164pub struct InitAccount<'info, T: crate::layout::LayoutContract> {
165 inner: &'info AccountView,
166 _ty: PhantomData<T>,
167}
168
169impl<'info, T: crate::layout::LayoutContract> Clone for InitAccount<'info, T> {
170 fn clone(&self) -> Self {
171 *self
172 }
173}
174impl<'info, T: crate::layout::LayoutContract> Copy for InitAccount<'info, T> {}
175
176impl<'info, T: crate::layout::LayoutContract> InitAccount<'info, T> {
177 /// Wrap an `AccountView` slot that will be created + initialised
178 /// by a lifecycle helper later in this instruction. Unsafe
179 /// because no state invariants hold for the account at wrap time.
180 #[inline(always)]
181 pub unsafe fn new_unchecked(view: &'info AccountView) -> Self {
182 Self {
183 inner: view,
184 _ty: PhantomData,
185 }
186 }
187
188 /// The underlying account view.
189 #[inline(always)]
190 pub fn as_account(&self) -> &'info AccountView {
191 self.inner
192 }
193
194 /// After `init_{field}()` has run, load the freshly-initialised
195 /// layout for reads / writes. The caller is responsible for
196 /// ordering this after the lifecycle helper.
197 #[inline(always)]
198 pub fn load_after_init(&self) -> Result<crate::borrow::RefMut<'_, T>, crate::error::ProgramError> {
199 self.inner.load_mut::<T>()
200 }
201}
202
203/// Account that must be a named program. `P: ProgramId` identifies
204/// which program the account's address must equal.
205///
206/// ```ignore
207/// pub system_program: Program<'info, SystemId>,
208/// ```
209#[repr(transparent)]
210pub struct Program<'info, P: ProgramId> {
211 inner: &'info AccountView,
212 _ty: PhantomData<P>,
213}
214
215impl<'info, P: ProgramId> Clone for Program<'info, P> {
216 fn clone(&self) -> Self {
217 *self
218 }
219}
220impl<'info, P: ProgramId> Copy for Program<'info, P> {}
221
222impl<'info, P: ProgramId> Program<'info, P> {
223 /// Wrap with address-pin and executable-flag verification.
224 #[inline]
225 pub fn try_new(view: &'info AccountView) -> Result<Self, crate::error::ProgramError> {
226 if view.address() != &P::ID {
227 return Err(crate::error::ProgramError::IncorrectProgramId);
228 }
229 if !view.executable() {
230 return Err(crate::error::ProgramError::InvalidAccountData);
231 }
232 Ok(Self {
233 inner: view,
234 _ty: PhantomData,
235 })
236 }
237
238 #[inline(always)]
239 pub fn as_account(&self) -> &'info AccountView {
240 self.inner
241 }
242}
243
244/// Marker trait for a compile-time-known program ID.
245///
246/// Callers wire programs into Hopper contexts by implementing this on
247/// a unit struct; the canonical names (`SystemId`, `TokenId`,
248/// `AssociatedTokenId`, `Token2022Id`) are provided below for the
249/// Solana programs most Hopper programs depend on.
250pub trait ProgramId: 'static {
251 const ID: Address;
252}
253
254/// Solana System Program.
255pub struct SystemId;
256impl ProgramId for SystemId {
257 const ID: Address = Address::new_from_array([0u8; 32]);
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn signer_wrapper_is_pointer_sized_zero_cost() {
266 // `#[repr(transparent)]` guarantees the wrapper has the same
267 // ABI as `&AccountView`. This test is a compile-time
268 // assertion via `size_of`.
269 assert_eq!(
270 core::mem::size_of::<Signer<'static>>(),
271 core::mem::size_of::<&'static AccountView>()
272 );
273 }
274
275 #[test]
276 fn system_program_id_is_all_zero() {
277 let sys = SystemId::ID;
278 assert_eq!(sys.as_array(), &[0u8; 32]);
279 }
280}