Skip to main content

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}