Skip to main content

hopper_runtime/
account_wrappers.rs

1//! Typed account wrappers for `#[derive(Accounts)]` and Hopper
2//! context lowering.
3//!
4//! These are thin, type-directed wrappers that
5//! programs can use in context structs to
6//! name an account's *role* rather than paint it with an
7//! `#[account(signer)]` attribute.
8//!
9//! ```ignore
10//! #[derive(Accounts)]
11//! pub struct Deposit<'info> {
12//!     pub authority: Signer<'info>,
13//!     pub vault: Account<'info, Vault>,
14//!     pub system_program: Program<'info, SystemId>,
15//! }
16//! ```
17//!
18//! The derive/context lowering recognizes these type names via
19//! `skips_layout_validation` and auto-derives the appropriate
20//! checks (`check_signer`, `check_owned_by`, `check_executable`,
21//! address-pin). The wrappers themselves are
22//! `#[repr(transparent)]` over `&AccountView`, giving them the same
23//! representation as the wrapped reference.
24//!
25//! # Why wrappers alongside the attribute path
26//!
27//! The attribute-directed lowering (`#[account(signer, mut)]`) and
28//! the wrapper-directed lowering (`pub authority: Signer<'info>`)
29//! both cover the same safety story. The wrapper form is
30//! Anchor-familiar and makes the role visible in every signature
31//! that accepts the account; the attribute form stays available for
32//! callers who prefer explicit constraint-lists. Both paths flow
33//! through the same canonical runtime checks. There is no
34//! duplicate safety implementation.
35
36use core::marker::PhantomData;
37
38use crate::account::AccountView;
39use crate::address::Address;
40
41/// Account that must be a transaction signer.
42///
43/// Hopper's `#[derive(Accounts)]` / context lowering treats a `Signer<'info>`
44/// field identically to `#[account(signer)] pub x: AccountView`. The emitted
45/// validation calls `check_signer()`.
46#[repr(transparent)]
47#[derive(Clone, Copy)]
48pub struct Signer<'info> {
49    inner: &'info AccountView<'info>,
50}
51
52impl<'info> Signer<'info> {
53    /// Wrap an `AccountView` that has already been verified as a
54    /// signer. The macro-generated `validate_{field}()` call emits
55    /// the `check_signer` first, so by the time the wrapper is
56    /// constructed the invariant already holds.
57    #[inline(always)]
58    ///
59    /// # Safety
60    ///
61    /// Caller must uphold the invariants documented for this unsafe API before invoking it.
62    pub unsafe fn new_unchecked(view: &'info AccountView<'info>) -> Self {
63        Self { inner: view }
64    }
65
66    /// Wrap an `AccountView` after verifying the signer invariant.
67    /// Prefer the macro-emitted `validate_{field}()` path when the
68    /// account is part of a `#[derive(Accounts)]` context struct.
69    #[inline]
70    pub fn try_new(view: &'info AccountView<'info>) -> Result<Self, crate::error::ProgramError> {
71        view.check_signer()?;
72        Ok(Self { inner: view })
73    }
74
75    /// The underlying account view.
76    #[inline(always)]
77    pub fn as_account(&self) -> &'info AccountView<'info> {
78        self.inner
79    }
80
81    /// The signer's public key.
82    #[inline(always)]
83    pub fn key(&self) -> &Address {
84        self.inner.address()
85    }
86}
87
88impl<'info> core::ops::Deref for Signer<'info> {
89    type Target = AccountView<'info>;
90    #[inline(always)]
91    fn deref(&self) -> &AccountView<'info> {
92        self.inner
93    }
94}
95
96/// Account with a verified Hopper layout owned by the executing program.
97///
98/// `Account<'info, T>` expands to the same checks as
99/// `#[account]` with `layout = T`: `check_owned_by(program_id)` +
100/// `load::<T>()` (which verifies the header, discriminator, version,
101/// and wire fingerprint). Field access is through `get()` / `get_mut()`
102/// which return typed references into the borrowed account data.
103#[repr(transparent)]
104pub struct Account<'info, T: crate::layout::LayoutContract + crate::Pod> {
105    inner: &'info AccountView<'info>,
106    _ty: PhantomData<T>,
107}
108
109impl<'info, T: crate::layout::LayoutContract + crate::Pod> Clone for Account<'info, T> {
110    fn clone(&self) -> Self {
111        *self
112    }
113}
114impl<'info, T: crate::layout::LayoutContract + crate::Pod> Copy for Account<'info, T> {}
115
116impl<'info, T: crate::layout::LayoutContract + crate::Pod> Account<'info, T> {
117    /// Wrap an already-validated `AccountView`. Unsafe because the
118    /// caller must have verified owner + layout header.
119    #[inline(always)]
120    ///
121    /// # Safety
122    ///
123    /// Caller must uphold the invariants documented for this unsafe API before invoking it.
124    pub unsafe fn new_unchecked(view: &'info AccountView<'info>) -> Self {
125        Self {
126            inner: view,
127            _ty: PhantomData,
128        }
129    }
130
131    /// Wrap with owner + layout verification.
132    #[inline]
133    pub fn try_new(
134        view: &'info AccountView<'info>,
135        owner: &Address,
136    ) -> Result<Self, crate::error::ProgramError> {
137        view.check_owned_by(owner)?;
138        let _ = view.load::<T>()?;
139        Ok(Self {
140            inner: view,
141            _ty: PhantomData,
142        })
143    }
144
145    /// The underlying account view.
146    #[inline(always)]
147    pub fn as_account(&self) -> &'info AccountView<'info> {
148        self.inner
149    }
150
151    /// The account public key.
152    #[inline(always)]
153    pub fn key(&self) -> &Address {
154        self.inner.address()
155    }
156
157    /// Borrow the typed layout for reading.
158    #[inline(always)]
159    pub fn load(&self) -> Result<crate::borrow::Ref<'_, T>, crate::error::ProgramError> {
160        self.inner.load::<T>()
161    }
162
163    /// Friendly alias for [`Self::load`].
164    #[inline(always)]
165    pub fn get(&self) -> Result<crate::borrow::Ref<'_, T>, crate::error::ProgramError> {
166        self.load()
167    }
168
169    /// Borrow the typed layout for the duration of a closure.
170    ///
171    /// The borrow guard stays scoped to the closure, so handlers can keep the
172    /// Quasar/Anchor-simple shape without bypassing Hopper's validation path.
173    #[inline]
174    pub fn with<R, F>(&self, f: F) -> Result<R, crate::error::ProgramError>
175    where
176        F: FnOnce(&T) -> Result<R, crate::error::ProgramError>,
177    {
178        self.inner.with::<T, R, F>(f)
179    }
180
181    /// Borrow the typed layout for writing.
182    ///
183    /// This takes `&self` because `Account<'info, T>` is a transparent
184    /// role wrapper over `AccountView`. Mutable exclusivity is enforced
185    /// by the account data borrow guard and Hopper borrow registry, so
186    /// copying the wrapper cannot create aliased writable access.
187    #[inline(always)]
188    pub fn load_mut(&self) -> Result<crate::borrow::RefMut<'_, T>, crate::error::ProgramError> {
189        self.inner.load_mut::<T>()
190    }
191
192    /// Friendly alias for [`Self::load_mut`].
193    #[inline(always)]
194    pub fn get_mut(&self) -> Result<crate::borrow::RefMut<'_, T>, crate::error::ProgramError> {
195        self.load_mut()
196    }
197
198    /// Mutably borrow the typed layout for the duration of a closure.
199    ///
200    /// This is the first-touch mutation sugar:
201    /// `ctx.accounts.counter.with_mut(|counter| counter.value.checked_add_assign(1))?;`
202    #[inline]
203    pub fn with_mut<R, F>(&self, f: F) -> Result<R, crate::error::ProgramError>
204    where
205        F: FnOnce(&mut T) -> Result<R, crate::error::ProgramError>,
206    {
207        self.inner.with_mut::<T, R, F>(f)
208    }
209}
210
211impl<'info, T: crate::layout::LayoutContract + crate::Pod> core::ops::Deref for Account<'info, T> {
212    type Target = AccountView<'info>;
213
214    #[inline(always)]
215    fn deref(&self) -> &AccountView<'info> {
216        self.inner
217    }
218}
219
220/// Account that is expected to be *created* during this instruction.
221///
222/// `InitAccount<'info, T>` skips the layout-header check at validation
223/// time (there's nothing to validate yet. the CPI hasn't run) but
224/// otherwise behaves like `Account<'info, T>`. Hopper context lowering pairs it
225/// with `#[account(init, payer = ..., space = ...)]` to emit the
226/// `init_{field}()` lifecycle helper that actually performs the System Program
227/// CPI.
228#[repr(transparent)]
229pub struct InitAccount<'info, T: crate::layout::LayoutContract + crate::Pod> {
230    inner: &'info AccountView<'info>,
231    _ty: PhantomData<T>,
232}
233
234impl<'info, T: crate::layout::LayoutContract + crate::Pod> Clone for InitAccount<'info, T> {
235    fn clone(&self) -> Self {
236        *self
237    }
238}
239impl<'info, T: crate::layout::LayoutContract + crate::Pod> Copy for InitAccount<'info, T> {}
240
241impl<'info, T: crate::layout::LayoutContract + crate::Pod> InitAccount<'info, T> {
242    /// Wrap an `AccountView` slot that will be created + initialised
243    /// by a lifecycle helper later in this instruction. Unsafe
244    /// because no state invariants hold for the account at wrap time.
245    #[inline(always)]
246    ///
247    /// # Safety
248    ///
249    /// Caller must uphold the invariants documented for this unsafe API before invoking it.
250    pub unsafe fn new_unchecked(view: &'info AccountView<'info>) -> Self {
251        Self {
252            inner: view,
253            _ty: PhantomData,
254        }
255    }
256
257    /// The underlying account view.
258    #[inline(always)]
259    pub fn as_account(&self) -> &'info AccountView<'info> {
260        self.inner
261    }
262
263    /// The account public key.
264    #[inline(always)]
265    pub fn key(&self) -> &Address {
266        self.inner.address()
267    }
268
269    /// After `init_{field}()` has run, load the freshly-initialised
270    /// layout for reads / writes. The caller is responsible for
271    /// ordering this after the lifecycle helper.
272    #[inline(always)]
273    pub fn load_after_init(
274        &self,
275    ) -> Result<crate::borrow::RefMut<'_, T>, crate::error::ProgramError> {
276        self.inner.load_mut::<T>()
277    }
278
279    /// Friendly alias for [`Self::load_after_init`].
280    #[inline(always)]
281    pub fn get_mut_after_init(
282        &self,
283    ) -> Result<crate::borrow::RefMut<'_, T>, crate::error::ProgramError> {
284        self.load_after_init()
285    }
286
287    /// Anchor-compatible alias for [`Self::load_after_init`].
288    #[inline(always)]
289    pub fn load_init(&self) -> Result<crate::borrow::RefMut<'_, T>, crate::error::ProgramError> {
290        self.load_after_init()
291    }
292
293    /// Mutably borrow the freshly-initialised layout for the duration of a closure.
294    #[inline]
295    pub fn with_mut_after_init<R, F>(&self, f: F) -> Result<R, crate::error::ProgramError>
296    where
297        F: FnOnce(&mut T) -> Result<R, crate::error::ProgramError>,
298    {
299        self.inner.with_mut::<T, R, F>(f)
300    }
301}
302
303impl<'info, T: crate::layout::LayoutContract + crate::Pod> core::ops::Deref
304    for InitAccount<'info, T>
305{
306    type Target = AccountView<'info>;
307
308    #[inline(always)]
309    fn deref(&self) -> &AccountView<'info> {
310        self.inner
311    }
312}
313
314/// Account with no role or layout validation.
315///
316/// Use this when a context needs a raw account in the `ctx.accounts.*`
317/// facade while keeping the role explicit in the type signature. Add
318/// field-level constraints such as `#[account(mut)]`, `owner = ...`, or
319/// `address = ...` when the account must satisfy additional checks.
320#[repr(transparent)]
321#[derive(Clone, Copy)]
322pub struct UncheckedAccount<'info> {
323    inner: &'info AccountView<'info>,
324}
325
326impl<'info> UncheckedAccount<'info> {
327    /// Wrap without validation.
328    #[inline(always)]
329    ///
330    /// # Safety
331    ///
332    /// Caller must ensure any required invariants are checked elsewhere.
333    pub unsafe fn new_unchecked(view: &'info AccountView<'info>) -> Self {
334        Self { inner: view }
335    }
336
337    /// Wrap without validation. This is intentionally explicit at the
338    /// type level: `UncheckedAccount` means no role has been proven.
339    #[inline(always)]
340    pub fn new(view: &'info AccountView<'info>) -> Self {
341        Self { inner: view }
342    }
343
344    /// The underlying account view.
345    #[inline(always)]
346    pub fn as_account(&self) -> &'info AccountView<'info> {
347        self.inner
348    }
349
350    /// The account public key.
351    #[inline(always)]
352    pub fn key(&self) -> &Address {
353        self.inner.address()
354    }
355}
356
357impl<'info> core::ops::Deref for UncheckedAccount<'info> {
358    type Target = AccountView<'info>;
359
360    #[inline(always)]
361    fn deref(&self) -> &AccountView<'info> {
362        self.inner
363    }
364}
365
366/// Account owned by the System Program.
367#[repr(transparent)]
368#[derive(Clone, Copy)]
369pub struct SystemAccount<'info> {
370    inner: &'info AccountView<'info>,
371}
372
373impl<'info> SystemAccount<'info> {
374    /// Wrap after verifying System Program ownership.
375    #[inline]
376    pub fn try_new(view: &'info AccountView<'info>) -> Result<Self, crate::error::ProgramError> {
377        view.check_owned_by(&SystemId::ID)?;
378        Ok(Self { inner: view })
379    }
380
381    /// Wrap an already-verified system-owned account.
382    #[inline(always)]
383    ///
384    /// # Safety
385    ///
386    /// Caller must have verified the account is owned by the System Program.
387    pub unsafe fn new_unchecked(view: &'info AccountView<'info>) -> Self {
388        Self { inner: view }
389    }
390
391    /// The underlying account view.
392    #[inline(always)]
393    pub fn as_account(&self) -> &'info AccountView<'info> {
394        self.inner
395    }
396
397    /// The account public key.
398    #[inline(always)]
399    pub fn key(&self) -> &Address {
400        self.inner.address()
401    }
402}
403
404impl<'info> core::ops::Deref for SystemAccount<'info> {
405    type Target = AccountView<'info>;
406
407    #[inline(always)]
408    fn deref(&self) -> &AccountView<'info> {
409        self.inner
410    }
411}
412
413/// Account that must be a named program. `P: ProgramId` identifies
414/// which program the account's address must equal.
415///
416/// ```ignore
417/// pub system_program: Program<'info, SystemId>,
418/// ```
419#[repr(transparent)]
420pub struct Program<'info, P: ProgramId> {
421    inner: &'info AccountView<'info>,
422    _ty: PhantomData<P>,
423}
424
425impl<'info, P: ProgramId> Clone for Program<'info, P> {
426    fn clone(&self) -> Self {
427        *self
428    }
429}
430impl<'info, P: ProgramId> Copy for Program<'info, P> {}
431
432impl<'info, P: ProgramId> Program<'info, P> {
433    /// Wrap with address-pin and executable-flag verification.
434    #[inline]
435    pub fn try_new(view: &'info AccountView<'info>) -> Result<Self, crate::error::ProgramError> {
436        if view.address() != &P::ID {
437            return Err(crate::error::ProgramError::IncorrectProgramId);
438        }
439        if !view.executable() {
440            return Err(crate::error::ProgramError::InvalidAccountData);
441        }
442        Ok(Self {
443            inner: view,
444            _ty: PhantomData,
445        })
446    }
447
448    #[inline(always)]
449    pub fn as_account(&self) -> &'info AccountView<'info> {
450        self.inner
451    }
452
453    /// The program account public key.
454    #[inline(always)]
455    pub fn key(&self) -> &Address {
456        self.inner.address()
457    }
458}
459
460impl<'info, P: ProgramId> core::ops::Deref for Program<'info, P> {
461    type Target = AccountView<'info>;
462
463    #[inline(always)]
464    fn deref(&self) -> &AccountView<'info> {
465        self.inner
466    }
467}
468
469/// Compile-time owner/program set for generic interface wrappers.
470///
471/// Implement this on a zero-sized marker type when a program context accepts
472/// one of several compatible programs. `Interface<'info, I>` validates an
473/// executable program account by key against this set, while
474/// `InterfaceAccount<'info, T>` validates account ownership through
475/// `T::Interface`.
476pub trait InterfaceSpec: 'static {
477    /// Program IDs accepted by this interface.
478    const IDS: &'static [Address];
479
480    /// Whether `program_id` belongs to this interface.
481    #[inline(always)]
482    fn contains(program_id: &Address) -> bool {
483        Self::IDS.iter().any(|candidate| candidate == program_id)
484    }
485}
486
487/// Hopper layout whose owner may be any program in an interface set.
488///
489/// This is the generic counterpart to token-specific interface helpers.
490/// Use it for Hopper-header layouts shared across compatible programs:
491///
492/// ```ignore
493/// pub struct VaultPrograms;
494/// impl InterfaceSpec for VaultPrograms {
495///     const IDS: &'static [Address] = &[PROGRAM_A, PROGRAM_B];
496/// }
497///
498/// impl InterfaceAccountLayout for SharedVault {
499///     type Interface = VaultPrograms;
500/// }
501/// ```
502pub trait InterfaceAccountLayout: crate::layout::LayoutContract {
503    /// The owner/program set accepted for this layout.
504    type Interface: InterfaceSpec;
505
506    /// Validate this interface account's bytes after owner-set validation.
507    ///
508    /// Concrete Hopper layouts keep the default: validate and borrow through
509    /// layout metadata checks (discriminator/version/layout id + required
510    /// length) without requiring the owner to be the executing program. Marker
511    /// interface layouts can override this to accept a bounded set of concrete
512    /// layout variants while still using the same `InterfaceAccount` wrapper.
513    #[inline]
514    fn validate_interface_account(
515        view: &AccountView<'_>,
516    ) -> Result<(), crate::error::ProgramError> {
517        let info = view
518            .layout_info()
519            .ok_or(crate::error::ProgramError::InvalidAccountData)?;
520        if !info.matches::<Self>() {
521            return Err(crate::error::ProgramError::InvalidAccountData);
522        }
523        if view.data_len() < Self::required_len() {
524            return Err(crate::error::ProgramError::AccountDataTooSmall);
525        }
526        Ok(())
527    }
528}
529
530/// Runtime resolver for marker interface account layouts.
531///
532/// Implement this for an `InterfaceAccountLayout` marker when one account slot
533/// may legally hold several concrete Hopper layouts, for example a migration
534/// reader that accepts `VaultV1` or `VaultV2`. The marker's
535/// `validate_interface_account` should accept exactly the same variants that
536/// `resolve` can return.
537pub trait InterfaceAccountResolve: InterfaceAccountLayout {
538    /// Borrowed resolved view returned by [`InterfaceAccount::resolve`].
539    type Resolved<'a>
540    where
541        Self: 'a;
542
543    /// Resolve the account bytes to a concrete borrowed variant.
544    fn resolve<'a>(
545        view: &'a AccountView<'a>,
546    ) -> Result<Self::Resolved<'a>, crate::error::ProgramError>;
547}
548
549/// Executable program account whose key is one of an interface's program IDs.
550#[repr(transparent)]
551pub struct Interface<'info, I: InterfaceSpec> {
552    inner: &'info AccountView<'info>,
553    _ty: PhantomData<I>,
554}
555
556impl<'info, I: InterfaceSpec> Clone for Interface<'info, I> {
557    fn clone(&self) -> Self {
558        *self
559    }
560}
561impl<'info, I: InterfaceSpec> Copy for Interface<'info, I> {}
562
563impl<'info, I: InterfaceSpec> Interface<'info, I> {
564    /// Wrap an already-validated interface program account.
565    #[inline(always)]
566    ///
567    /// # Safety
568    ///
569    /// Caller must have verified the account address is in `I::IDS` and the
570    /// account is executable.
571    pub unsafe fn new_unchecked(view: &'info AccountView<'info>) -> Self {
572        Self {
573            inner: view,
574            _ty: PhantomData,
575        }
576    }
577
578    /// Wrap after verifying address membership and executability.
579    #[inline]
580    pub fn try_new(view: &'info AccountView<'info>) -> Result<Self, crate::error::ProgramError> {
581        if !I::contains(view.address()) {
582            return Err(crate::error::ProgramError::IncorrectProgramId);
583        }
584        if !view.executable() {
585            return Err(crate::error::ProgramError::InvalidAccountData);
586        }
587        Ok(Self {
588            inner: view,
589            _ty: PhantomData,
590        })
591    }
592
593    /// The underlying account view.
594    #[inline(always)]
595    pub fn as_account(&self) -> &'info AccountView<'info> {
596        self.inner
597    }
598
599    /// The selected program id.
600    #[inline(always)]
601    pub fn key(&self) -> &Address {
602        self.inner.address()
603    }
604}
605
606impl<'info, I: InterfaceSpec> core::ops::Deref for Interface<'info, I> {
607    type Target = AccountView<'info>;
608
609    #[inline(always)]
610    fn deref(&self) -> &AccountView<'info> {
611        self.inner
612    }
613}
614
615/// Hopper-layout account owned by one of a declared interface's programs.
616///
617/// `InterfaceAccount<'info, T>` validates two things before binding:
618/// account owner is in `T::Interface::IDS`, and the account bytes match
619/// `T`'s Hopper layout header. Reads use `load_cross_program` so ownership is
620/// intentionally decoupled from the executing program.
621#[repr(transparent)]
622pub struct InterfaceAccount<'info, T: InterfaceAccountLayout> {
623    inner: &'info AccountView<'info>,
624    _ty: PhantomData<T>,
625}
626
627impl<'info, T: InterfaceAccountLayout> Clone for InterfaceAccount<'info, T> {
628    fn clone(&self) -> Self {
629        *self
630    }
631}
632impl<'info, T: InterfaceAccountLayout> Copy for InterfaceAccount<'info, T> {}
633
634impl<'info, T: InterfaceAccountLayout> InterfaceAccount<'info, T> {
635    #[inline(always)]
636    fn revalidate(&self) -> Result<(), crate::error::ProgramError> {
637        let owner = self.inner.read_owner();
638        if !<T::Interface as InterfaceSpec>::contains(&owner) {
639            return Err(crate::error::ProgramError::IncorrectProgramId);
640        }
641        T::validate_interface_account(self.inner)
642    }
643
644    /// Wrap an already-validated interface-owned layout account.
645    #[inline(always)]
646    ///
647    /// # Safety
648    ///
649    /// Caller must have verified owner membership and layout identity.
650    pub unsafe fn new_unchecked(view: &'info AccountView<'info>) -> Self {
651        Self {
652            inner: view,
653            _ty: PhantomData,
654        }
655    }
656
657    /// Wrap after verifying owner membership and layout identity.
658    #[inline]
659    pub fn try_new(view: &'info AccountView<'info>) -> Result<Self, crate::error::ProgramError> {
660        let owner = view.read_owner();
661        if !<T::Interface as InterfaceSpec>::contains(&owner) {
662            return Err(crate::error::ProgramError::IncorrectProgramId);
663        }
664        T::validate_interface_account(view)?;
665        Ok(Self {
666            inner: view,
667            _ty: PhantomData,
668        })
669    }
670
671    /// The underlying account view.
672    #[inline(always)]
673    pub fn as_account(&self) -> &'info AccountView<'info> {
674        self.inner
675    }
676
677    /// The account public key.
678    #[inline(always)]
679    pub fn key(&self) -> &Address {
680        self.inner.address()
681    }
682
683    /// The owning program, copied out of the account header.
684    #[inline(always)]
685    pub fn owner(&self) -> Address {
686        self.inner.read_owner()
687    }
688
689    /// Borrow the cross-program layout for reading.
690    #[inline(always)]
691    pub fn load(&self) -> Result<crate::borrow::Ref<'_, T>, crate::error::ProgramError>
692    where
693        T: crate::Pod,
694    {
695        self.revalidate()?;
696        self.inner.load_cross_program::<T>()
697    }
698
699    /// Friendly alias for [`Self::load`].
700    #[inline(always)]
701    pub fn get(&self) -> Result<crate::borrow::Ref<'_, T>, crate::error::ProgramError>
702    where
703        T: crate::Pod,
704    {
705        self.load()
706    }
707
708    /// Borrow the cross-program layout for the duration of a closure.
709    #[inline]
710    pub fn with<R, F>(&self, f: F) -> Result<R, crate::error::ProgramError>
711    where
712        T: crate::Pod,
713        F: FnOnce(&T) -> Result<R, crate::error::ProgramError>,
714    {
715        let account = self.load()?;
716        f(&*account)
717    }
718
719    /// Borrow the account as another concrete layout in the same interface set.
720    ///
721    /// This is useful for marker interface accounts whose validation accepts a
722    /// bounded set of compatible layouts. The associated-type equality keeps a
723    /// caller from accidentally loading a layout governed by a different owner
724    /// set.
725    #[inline(always)]
726    pub fn load_as<U>(&self) -> Result<crate::borrow::Ref<'_, U>, crate::error::ProgramError>
727    where
728        U: InterfaceAccountLayout<Interface = <T as InterfaceAccountLayout>::Interface>
729            + crate::Pod,
730    {
731        self.revalidate()?;
732        U::validate_interface_account(self.inner)?;
733        self.inner.load_cross_program::<U>()
734    }
735
736    /// Friendly alias for [`Self::load_as`].
737    #[inline(always)]
738    pub fn get_as<U>(&self) -> Result<crate::borrow::Ref<'_, U>, crate::error::ProgramError>
739    where
740        U: InterfaceAccountLayout<Interface = <T as InterfaceAccountLayout>::Interface>
741            + crate::Pod,
742    {
743        self.load_as::<U>()
744    }
745
746    /// Borrow another concrete interface layout for the duration of a closure.
747    #[inline]
748    pub fn with_as<U, R, F>(&self, f: F) -> Result<R, crate::error::ProgramError>
749    where
750        U: InterfaceAccountLayout<Interface = <T as InterfaceAccountLayout>::Interface>
751            + crate::Pod,
752        F: FnOnce(&U) -> Result<R, crate::error::ProgramError>,
753    {
754        let account = self.load_as::<U>()?;
755        f(&*account)
756    }
757
758    /// Whether the current bytes match another concrete layout in the same
759    /// interface set.
760    #[inline(always)]
761    pub fn is<U>(&self) -> bool
762    where
763        U: InterfaceAccountLayout<Interface = <T as InterfaceAccountLayout>::Interface>
764            + crate::Pod,
765    {
766        self.revalidate().is_ok()
767            && self
768                .inner
769                .layout_info()
770                .is_some_and(|info| info.matches::<U>())
771    }
772
773    /// Resolve a marker interface account to one of its concrete variants.
774    #[inline(always)]
775    pub fn resolve(&self) -> Result<T::Resolved<'_>, crate::error::ProgramError>
776    where
777        T: InterfaceAccountResolve,
778    {
779        self.revalidate()?;
780        T::resolve(self.inner)
781    }
782}
783
784impl<'info, T: InterfaceAccountLayout> core::ops::Deref for InterfaceAccount<'info, T> {
785    type Target = AccountView<'info>;
786
787    #[inline(always)]
788    fn deref(&self) -> &AccountView<'info> {
789        self.inner
790    }
791}
792
793/// Marker trait for a compile-time-known program ID.
794///
795/// Callers wire programs into Hopper contexts by implementing this on
796/// a unit struct; the canonical names (`SystemId`, `TokenId`,
797/// `AssociatedTokenId`, `Token2022Id`) are provided below for the
798/// Solana programs most Hopper programs depend on.
799pub trait ProgramId: 'static {
800    const ID: Address;
801}
802
803/// Solana System Program.
804pub struct SystemId;
805impl ProgramId for SystemId {
806    const ID: Address = Address::new_from_array([0u8; 32]);
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812
813    #[test]
814    fn signer_wrapper_is_pointer_sized_zero_cost() {
815        // `#[repr(transparent)]` guarantees the wrapper has the same
816        // ABI as `&AccountView`. This test is a compile-time
817        // assertion via `size_of`.
818        assert_eq!(
819            core::mem::size_of::<Signer<'static>>(),
820            core::mem::size_of::<&'static AccountView<'static>>()
821        );
822    }
823
824    #[test]
825    fn system_program_id_is_all_zero() {
826        let sys = SystemId::ID;
827        assert_eq!(sys.as_array(), &[0u8; 32]);
828    }
829}
830
831#[cfg(test)]
832mod resolver_tests {
833    use super::*;
834    use crate::layout::{HopperHeader, LayoutContract};
835
836    use hopper_native::{
837        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
838    };
839
840    const PROGRAM_A: Address = Address::new_from_array([0xA1; 32]);
841    const PROGRAM_B: Address = Address::new_from_array([0xB2; 32]);
842    const OTHER_PROGRAM: Address = Address::new_from_array([0xCC; 32]);
843
844    struct VaultPrograms;
845    impl InterfaceSpec for VaultPrograms {
846        const IDS: &'static [Address] = &[PROGRAM_A, PROGRAM_B];
847    }
848
849    #[repr(C)]
850    #[derive(Clone, Copy, Debug, Default)]
851    struct VaultV1 {
852        balance: [u8; 8],
853    }
854
855    unsafe impl crate::Zeroable for VaultV1 {}
856    unsafe impl crate::Pod for VaultV1 {}
857
858    impl crate::field_map::FieldMap for VaultV1 {
859        const FIELDS: &'static [crate::field_map::FieldInfo] = &[crate::field_map::FieldInfo::new(
860            "balance",
861            HopperHeader::SIZE,
862            8,
863        )];
864    }
865
866    impl LayoutContract for VaultV1 {
867        const DISC: u8 = 11;
868        const VERSION: u8 = 1;
869        const LAYOUT_ID: [u8; 8] = [0x11; 8];
870        const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
871    }
872
873    impl InterfaceAccountLayout for VaultV1 {
874        type Interface = VaultPrograms;
875    }
876
877    #[repr(C)]
878    #[derive(Clone, Copy, Debug, Default)]
879    struct VaultV2 {
880        balance: [u8; 8],
881        bump: [u8; 8],
882    }
883
884    unsafe impl crate::Zeroable for VaultV2 {}
885    unsafe impl crate::Pod for VaultV2 {}
886
887    impl crate::field_map::FieldMap for VaultV2 {
888        const FIELDS: &'static [crate::field_map::FieldInfo] = &[
889            crate::field_map::FieldInfo::new("balance", HopperHeader::SIZE, 8),
890            crate::field_map::FieldInfo::new("bump", HopperHeader::SIZE + 8, 8),
891        ];
892    }
893
894    impl LayoutContract for VaultV2 {
895        const DISC: u8 = 12;
896        const VERSION: u8 = 2;
897        const LAYOUT_ID: [u8; 8] = [0x22; 8];
898        const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
899    }
900
901    impl InterfaceAccountLayout for VaultV2 {
902        type Interface = VaultPrograms;
903    }
904
905    #[repr(C)]
906    #[derive(Clone, Copy, Debug, Default)]
907    struct AnyVault {
908        _reserved: [u8; 1],
909    }
910
911    unsafe impl crate::Zeroable for AnyVault {}
912    unsafe impl crate::Pod for AnyVault {}
913
914    impl crate::field_map::FieldMap for AnyVault {
915        const FIELDS: &'static [crate::field_map::FieldInfo] = &[];
916    }
917
918    impl LayoutContract for AnyVault {
919        const DISC: u8 = 0;
920        const VERSION: u8 = 0;
921        const LAYOUT_ID: [u8; 8] = [0; 8];
922        const SIZE: usize = HopperHeader::SIZE;
923    }
924
925    impl InterfaceAccountLayout for AnyVault {
926        type Interface = VaultPrograms;
927
928        fn validate_interface_account(
929            view: &AccountView<'_>,
930        ) -> Result<(), crate::error::ProgramError> {
931            let data = view.try_borrow()?;
932            if VaultV1::validate_header(&data).is_ok() || VaultV2::validate_header(&data).is_ok() {
933                Ok(())
934            } else {
935                Err(crate::error::ProgramError::InvalidAccountData)
936            }
937        }
938    }
939
940    enum ResolvedVault<'a> {
941        V1(crate::borrow::Ref<'a, VaultV1>),
942        V2(crate::borrow::Ref<'a, VaultV2>),
943    }
944
945    impl InterfaceAccountResolve for AnyVault {
946        type Resolved<'a> = ResolvedVault<'a>;
947
948        fn resolve<'a>(
949            view: &'a AccountView<'a>,
950        ) -> Result<Self::Resolved<'a>, crate::error::ProgramError> {
951            let info = view
952                .layout_info()
953                .ok_or(crate::error::ProgramError::AccountDataTooSmall)?;
954            if info.matches::<VaultV1>() {
955                return Ok(ResolvedVault::V1(view.load_cross_program::<VaultV1>()?));
956            }
957            if info.matches::<VaultV2>() {
958                return Ok(ResolvedVault::V2(view.load_cross_program::<VaultV2>()?));
959            }
960            Err(crate::error::ProgramError::InvalidAccountData)
961        }
962    }
963
964    fn make_account(
965        total_data_len: usize,
966        owner: Address,
967    ) -> (std::vec::Vec<u64>, AccountView<'static>) {
968        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + total_data_len).div_ceil(8)];
969        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
970        // SAFETY: The test owns `backing`, writes one RuntimeAccount header,
971        // and keeps the buffer alive for the returned AccountView.
972        unsafe {
973            raw.write(RuntimeAccount {
974                borrow_state: NOT_BORROWED,
975                is_signer: 1,
976                is_writable: 1,
977                executable: 0,
978                resize_delta: 0,
979                address: NativeAddress::new_from_array([0x44; 32]),
980                owner: NativeAddress::new_from_array(*owner.as_array()),
981                lamports: 42,
982                data_len: total_data_len as u64,
983            });
984        }
985        // SAFETY: `raw` points to the RuntimeAccount header initialized above.
986        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
987        (backing, AccountView::from_backend(backend))
988    }
989
990    #[test]
991    fn interface_account_resolves_bounded_layout_variants() {
992        let (_v1_backing, v1_account) = make_account(VaultV1::SIZE, PROGRAM_B);
993        {
994            let mut data = v1_account.try_borrow_mut().unwrap();
995            crate::layout::init_header::<VaultV1>(&mut data).unwrap();
996            data[HopperHeader::SIZE..HopperHeader::SIZE + 8].copy_from_slice(&300u64.to_le_bytes());
997        }
998
999        let v1_vault = InterfaceAccount::<AnyVault>::try_new(&v1_account).unwrap();
1000        match v1_vault.resolve().unwrap() {
1001            ResolvedVault::V1(v1) => {
1002                assert_eq!(u64::from_le_bytes(v1.balance), 300);
1003            }
1004            ResolvedVault::V2(_) => panic!("expected v1"),
1005        }
1006
1007        let (_backing, account) = make_account(VaultV2::SIZE, PROGRAM_A);
1008        {
1009            let mut data = account.try_borrow_mut().unwrap();
1010            crate::layout::init_header::<VaultV2>(&mut data).unwrap();
1011            data[HopperHeader::SIZE..HopperHeader::SIZE + 8].copy_from_slice(&700u64.to_le_bytes());
1012            data[HopperHeader::SIZE + 8..HopperHeader::SIZE + 16]
1013                .copy_from_slice(&9u64.to_le_bytes());
1014        }
1015
1016        let vault = InterfaceAccount::<AnyVault>::try_new(&account).unwrap();
1017        assert!(vault.is::<VaultV2>());
1018        assert!(!vault.is::<VaultV1>());
1019
1020        match vault.resolve().unwrap() {
1021            ResolvedVault::V2(v2) => {
1022                assert_eq!(u64::from_le_bytes(v2.balance), 700);
1023                assert_eq!(u64::from_le_bytes(v2.bump), 9);
1024            }
1025            ResolvedVault::V1(_) => panic!("expected v2"),
1026        }
1027
1028        let v2 = vault.load_as::<VaultV2>().unwrap();
1029        assert_eq!(u64::from_le_bytes(v2.balance), 700);
1030        assert!(vault.get_as::<VaultV1>().is_err());
1031    }
1032
1033    #[test]
1034    fn interface_account_resolver_keeps_owner_and_layout_checks() {
1035        let (_wrong_owner_backing, wrong_owner) = make_account(VaultV1::SIZE, OTHER_PROGRAM);
1036        {
1037            let mut data = wrong_owner.try_borrow_mut().unwrap();
1038            crate::layout::init_header::<VaultV1>(&mut data).unwrap();
1039        }
1040        let wrong_owner_result = InterfaceAccount::<AnyVault>::try_new(&wrong_owner);
1041        assert!(matches!(
1042            wrong_owner_result,
1043            Err(crate::error::ProgramError::IncorrectProgramId)
1044        ));
1045
1046        let (_bad_layout_backing, bad_layout) = make_account(VaultV1::SIZE, PROGRAM_B);
1047        {
1048            let mut data = bad_layout.try_borrow_mut().unwrap();
1049            crate::layout::write_header(&mut data, 99, 1, &[0x99; 8]).unwrap();
1050        }
1051        let bad_layout_result = InterfaceAccount::<AnyVault>::try_new(&bad_layout);
1052        assert!(matches!(
1053            bad_layout_result,
1054            Err(crate::error::ProgramError::InvalidAccountData)
1055        ));
1056    }
1057
1058    #[test]
1059    fn interface_account_revalidates_owner_after_binding() {
1060        let (_backing, account) = make_account(VaultV1::SIZE, PROGRAM_A);
1061        {
1062            let mut data = account.try_borrow_mut().unwrap();
1063            crate::layout::init_header::<VaultV1>(&mut data).unwrap();
1064        }
1065        let vault = InterfaceAccount::<AnyVault>::try_new(&account).unwrap();
1066
1067        // Model a writable CPI to the original owner reassigning the account.
1068        // SAFETY: this test owns the synthetic RuntimeAccount backing and is
1069        // deliberately exercising the post-CPI owner-change boundary.
1070        unsafe {
1071            account.assign(&OTHER_PROGRAM);
1072        }
1073
1074        assert!(matches!(
1075            vault.load_as::<VaultV1>(),
1076            Err(crate::error::ProgramError::IncorrectProgramId)
1077        ));
1078        assert!(matches!(
1079            vault.resolve(),
1080            Err(crate::error::ProgramError::IncorrectProgramId)
1081        ));
1082        assert!(!vault.is::<VaultV1>());
1083    }
1084
1085    #[test]
1086    fn interface_account_revalidates_layout_after_binding() {
1087        // Leave enough capacity for either layout so the failure proves type
1088        // identity was rechecked, rather than merely tripping a length guard.
1089        let (_backing, account) = make_account(VaultV2::SIZE, PROGRAM_A);
1090        {
1091            let mut data = account.try_borrow_mut().unwrap();
1092            crate::layout::init_header::<VaultV1>(&mut data).unwrap();
1093        }
1094        let vault = InterfaceAccount::<VaultV1>::try_new(&account).unwrap();
1095
1096        // Model the owning program changing the account variant during CPI.
1097        {
1098            let mut data = account.try_borrow_mut().unwrap();
1099            crate::layout::init_header::<VaultV2>(&mut data).unwrap();
1100        }
1101
1102        assert!(matches!(
1103            vault.load(),
1104            Err(crate::error::ProgramError::InvalidAccountData)
1105        ));
1106        assert!(!vault.is::<VaultV1>());
1107    }
1108}