Skip to main content

hopper_runtime/
account_wrappers.rs

1//! Anchor-grade typed account wrappers for `#[derive(Accounts)]` and Hopper
2//! context lowering.
3//!
4//! Closes Hopper Safety Audit Stage 2.3: zero-cost, zero-alignment,
5//! type-directed wrappers that 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` so they compile away
23//! to the same pointer access as the raw form.
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,
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) -> 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) -> 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 {
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;
90    #[inline(always)]
91    fn deref(&self) -> &AccountView {
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> {
105    inner: &'info AccountView,
106    _ty: PhantomData<T>,
107}
108
109impl<'info, T: crate::layout::LayoutContract> Clone for Account<'info, T> {
110    fn clone(&self) -> Self {
111        *self
112    }
113}
114impl<'info, T: crate::layout::LayoutContract> Copy for Account<'info, T> {}
115
116impl<'info, T: crate::layout::LayoutContract> 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) -> 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,
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 {
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> core::ops::Deref for Account<'info, T> {
212    type Target = AccountView;
213
214    #[inline(always)]
215    fn deref(&self) -> &AccountView {
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> {
230    inner: &'info AccountView,
231    _ty: PhantomData<T>,
232}
233
234impl<'info, T: crate::layout::LayoutContract> Clone for InitAccount<'info, T> {
235    fn clone(&self) -> Self {
236        *self
237    }
238}
239impl<'info, T: crate::layout::LayoutContract> Copy for InitAccount<'info, T> {}
240
241impl<'info, T: crate::layout::LayoutContract> 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) -> 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 {
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    /// Mutably borrow the freshly-initialised layout for the duration of a closure.
288    #[inline]
289    pub fn with_mut_after_init<R, F>(&self, f: F) -> Result<R, crate::error::ProgramError>
290    where
291        F: FnOnce(&mut T) -> Result<R, crate::error::ProgramError>,
292    {
293        self.inner.with_mut::<T, R, F>(f)
294    }
295}
296
297impl<'info, T: crate::layout::LayoutContract> core::ops::Deref for InitAccount<'info, T> {
298    type Target = AccountView;
299
300    #[inline(always)]
301    fn deref(&self) -> &AccountView {
302        self.inner
303    }
304}
305
306/// Account with no role or layout validation.
307///
308/// Use this when a context needs a raw account in the `ctx.accounts.*`
309/// facade while keeping the role explicit in the type signature. Add
310/// field-level constraints such as `#[account(mut)]`, `owner = ...`, or
311/// `address = ...` when the account must satisfy additional checks.
312#[repr(transparent)]
313#[derive(Clone, Copy)]
314pub struct UncheckedAccount<'info> {
315    inner: &'info AccountView,
316}
317
318impl<'info> UncheckedAccount<'info> {
319    /// Wrap without validation.
320    #[inline(always)]
321    ///
322    /// # Safety
323    ///
324    /// Caller must ensure any required invariants are checked elsewhere.
325    pub unsafe fn new_unchecked(view: &'info AccountView) -> Self {
326        Self { inner: view }
327    }
328
329    /// Wrap without validation. This is intentionally explicit at the
330    /// type level: `UncheckedAccount` means no role has been proven.
331    #[inline(always)]
332    pub fn new(view: &'info AccountView) -> Self {
333        Self { inner: view }
334    }
335
336    /// The underlying account view.
337    #[inline(always)]
338    pub fn as_account(&self) -> &'info AccountView {
339        self.inner
340    }
341
342    /// The account public key.
343    #[inline(always)]
344    pub fn key(&self) -> &Address {
345        self.inner.address()
346    }
347}
348
349impl<'info> core::ops::Deref for UncheckedAccount<'info> {
350    type Target = AccountView;
351
352    #[inline(always)]
353    fn deref(&self) -> &AccountView {
354        self.inner
355    }
356}
357
358/// Account owned by the System Program.
359#[repr(transparent)]
360#[derive(Clone, Copy)]
361pub struct SystemAccount<'info> {
362    inner: &'info AccountView,
363}
364
365impl<'info> SystemAccount<'info> {
366    /// Wrap after verifying System Program ownership.
367    #[inline]
368    pub fn try_new(view: &'info AccountView) -> Result<Self, crate::error::ProgramError> {
369        view.check_owned_by(&SystemId::ID)?;
370        Ok(Self { inner: view })
371    }
372
373    /// Wrap an already-verified system-owned account.
374    #[inline(always)]
375    ///
376    /// # Safety
377    ///
378    /// Caller must have verified the account is owned by the System Program.
379    pub unsafe fn new_unchecked(view: &'info AccountView) -> Self {
380        Self { inner: view }
381    }
382
383    /// The underlying account view.
384    #[inline(always)]
385    pub fn as_account(&self) -> &'info AccountView {
386        self.inner
387    }
388
389    /// The account public key.
390    #[inline(always)]
391    pub fn key(&self) -> &Address {
392        self.inner.address()
393    }
394}
395
396impl<'info> core::ops::Deref for SystemAccount<'info> {
397    type Target = AccountView;
398
399    #[inline(always)]
400    fn deref(&self) -> &AccountView {
401        self.inner
402    }
403}
404
405/// Account that must be a named program. `P: ProgramId` identifies
406/// which program the account's address must equal.
407///
408/// ```ignore
409/// pub system_program: Program<'info, SystemId>,
410/// ```
411#[repr(transparent)]
412pub struct Program<'info, P: ProgramId> {
413    inner: &'info AccountView,
414    _ty: PhantomData<P>,
415}
416
417impl<'info, P: ProgramId> Clone for Program<'info, P> {
418    fn clone(&self) -> Self {
419        *self
420    }
421}
422impl<'info, P: ProgramId> Copy for Program<'info, P> {}
423
424impl<'info, P: ProgramId> Program<'info, P> {
425    /// Wrap with address-pin and executable-flag verification.
426    #[inline]
427    pub fn try_new(view: &'info AccountView) -> Result<Self, crate::error::ProgramError> {
428        if view.address() != &P::ID {
429            return Err(crate::error::ProgramError::IncorrectProgramId);
430        }
431        if !view.executable() {
432            return Err(crate::error::ProgramError::InvalidAccountData);
433        }
434        Ok(Self {
435            inner: view,
436            _ty: PhantomData,
437        })
438    }
439
440    #[inline(always)]
441    pub fn as_account(&self) -> &'info AccountView {
442        self.inner
443    }
444
445    /// The program account public key.
446    #[inline(always)]
447    pub fn key(&self) -> &Address {
448        self.inner.address()
449    }
450}
451
452impl<'info, P: ProgramId> core::ops::Deref for Program<'info, P> {
453    type Target = AccountView;
454
455    #[inline(always)]
456    fn deref(&self) -> &AccountView {
457        self.inner
458    }
459}
460
461/// Compile-time owner/program set for generic interface wrappers.
462///
463/// Implement this on a zero-sized marker type when a program context accepts
464/// one of several compatible programs. `Interface<'info, I>` validates an
465/// executable program account by key against this set, while
466/// `InterfaceAccount<'info, T>` validates account ownership through
467/// `T::Interface`.
468pub trait InterfaceSpec: 'static {
469    /// Program IDs accepted by this interface.
470    const IDS: &'static [Address];
471
472    /// Whether `program_id` belongs to this interface.
473    #[inline(always)]
474    fn contains(program_id: &Address) -> bool {
475        Self::IDS.iter().any(|candidate| candidate == program_id)
476    }
477}
478
479/// Hopper layout whose owner may be any program in an interface set.
480///
481/// This is the generic counterpart to token-specific interface helpers.
482/// Use it for Hopper-header layouts shared across compatible programs:
483///
484/// ```ignore
485/// pub struct VaultPrograms;
486/// impl InterfaceSpec for VaultPrograms {
487///     const IDS: &'static [Address] = &[PROGRAM_A, PROGRAM_B];
488/// }
489///
490/// impl InterfaceAccountLayout for SharedVault {
491///     type Interface = VaultPrograms;
492/// }
493/// ```
494pub trait InterfaceAccountLayout: crate::layout::LayoutContract {
495    /// The owner/program set accepted for this layout.
496    type Interface: InterfaceSpec;
497
498    /// Validate this interface account's bytes after owner-set validation.
499    ///
500    /// Concrete Hopper layouts keep the default: validate and borrow through
501    /// the cross-program loader, which checks discriminator, layout id, and
502    /// size without requiring the owner to be the executing program. Marker
503    /// interface layouts can override this to accept a bounded set of concrete
504    /// layout variants while still using the same `InterfaceAccount` wrapper.
505    #[inline]
506    fn validate_interface_account(view: &AccountView) -> Result<(), crate::error::ProgramError> {
507        let _ = view.load_cross_program::<Self>()?;
508        Ok(())
509    }
510}
511
512/// Runtime resolver for marker interface account layouts.
513///
514/// Implement this for an `InterfaceAccountLayout` marker when one account slot
515/// may legally hold several concrete Hopper layouts, for example a migration
516/// reader that accepts `VaultV1` or `VaultV2`. The marker's
517/// `validate_interface_account` should accept exactly the same variants that
518/// `resolve` can return.
519pub trait InterfaceAccountResolve: InterfaceAccountLayout {
520    /// Borrowed resolved view returned by [`InterfaceAccount::resolve`].
521    type Resolved<'a>
522    where
523        Self: 'a;
524
525    /// Resolve the account bytes to a concrete borrowed variant.
526    fn resolve<'a>(view: &'a AccountView)
527        -> Result<Self::Resolved<'a>, crate::error::ProgramError>;
528}
529
530/// Executable program account whose key is one of an interface's program IDs.
531#[repr(transparent)]
532pub struct Interface<'info, I: InterfaceSpec> {
533    inner: &'info AccountView,
534    _ty: PhantomData<I>,
535}
536
537impl<'info, I: InterfaceSpec> Clone for Interface<'info, I> {
538    fn clone(&self) -> Self {
539        *self
540    }
541}
542impl<'info, I: InterfaceSpec> Copy for Interface<'info, I> {}
543
544impl<'info, I: InterfaceSpec> Interface<'info, I> {
545    /// Wrap an already-validated interface program account.
546    #[inline(always)]
547    ///
548    /// # Safety
549    ///
550    /// Caller must have verified the account address is in `I::IDS` and the
551    /// account is executable.
552    pub unsafe fn new_unchecked(view: &'info AccountView) -> Self {
553        Self {
554            inner: view,
555            _ty: PhantomData,
556        }
557    }
558
559    /// Wrap after verifying address membership and executability.
560    #[inline]
561    pub fn try_new(view: &'info AccountView) -> Result<Self, crate::error::ProgramError> {
562        if !I::contains(view.address()) {
563            return Err(crate::error::ProgramError::IncorrectProgramId);
564        }
565        if !view.executable() {
566            return Err(crate::error::ProgramError::InvalidAccountData);
567        }
568        Ok(Self {
569            inner: view,
570            _ty: PhantomData,
571        })
572    }
573
574    /// The underlying account view.
575    #[inline(always)]
576    pub fn as_account(&self) -> &'info AccountView {
577        self.inner
578    }
579
580    /// The selected program id.
581    #[inline(always)]
582    pub fn key(&self) -> &Address {
583        self.inner.address()
584    }
585}
586
587impl<'info, I: InterfaceSpec> core::ops::Deref for Interface<'info, I> {
588    type Target = AccountView;
589
590    #[inline(always)]
591    fn deref(&self) -> &AccountView {
592        self.inner
593    }
594}
595
596/// Hopper-layout account owned by one of a declared interface's programs.
597///
598/// `InterfaceAccount<'info, T>` validates two things before binding:
599/// account owner is in `T::Interface::IDS`, and the account bytes match
600/// `T`'s Hopper layout header. Reads use `load_cross_program` so ownership is
601/// intentionally decoupled from the executing program.
602#[repr(transparent)]
603pub struct InterfaceAccount<'info, T: InterfaceAccountLayout> {
604    inner: &'info AccountView,
605    _ty: PhantomData<T>,
606}
607
608impl<'info, T: InterfaceAccountLayout> Clone for InterfaceAccount<'info, T> {
609    fn clone(&self) -> Self {
610        *self
611    }
612}
613impl<'info, T: InterfaceAccountLayout> Copy for InterfaceAccount<'info, T> {}
614
615impl<'info, T: InterfaceAccountLayout> InterfaceAccount<'info, T> {
616    /// Wrap an already-validated interface-owned layout account.
617    #[inline(always)]
618    ///
619    /// # Safety
620    ///
621    /// Caller must have verified owner membership and layout identity.
622    pub unsafe fn new_unchecked(view: &'info AccountView) -> Self {
623        Self {
624            inner: view,
625            _ty: PhantomData,
626        }
627    }
628
629    /// Wrap after verifying owner membership and layout identity.
630    #[inline]
631    pub fn try_new(view: &'info AccountView) -> Result<Self, crate::error::ProgramError> {
632        let owner = view.read_owner();
633        if !<T::Interface as InterfaceSpec>::contains(&owner) {
634            return Err(crate::error::ProgramError::IncorrectProgramId);
635        }
636        T::validate_interface_account(view)?;
637        Ok(Self {
638            inner: view,
639            _ty: PhantomData,
640        })
641    }
642
643    /// The underlying account view.
644    #[inline(always)]
645    pub fn as_account(&self) -> &'info AccountView {
646        self.inner
647    }
648
649    /// The account public key.
650    #[inline(always)]
651    pub fn key(&self) -> &Address {
652        self.inner.address()
653    }
654
655    /// The owning program, copied out of the account header.
656    #[inline(always)]
657    pub fn owner(&self) -> Address {
658        self.inner.read_owner()
659    }
660
661    /// Borrow the cross-program layout for reading.
662    #[inline(always)]
663    pub fn load(&self) -> Result<crate::borrow::Ref<'_, T>, crate::error::ProgramError> {
664        self.inner.load_cross_program::<T>()
665    }
666
667    /// Friendly alias for [`Self::load`].
668    #[inline(always)]
669    pub fn get(&self) -> Result<crate::borrow::Ref<'_, T>, crate::error::ProgramError> {
670        self.load()
671    }
672
673    /// Borrow the cross-program layout for the duration of a closure.
674    #[inline]
675    pub fn with<R, F>(&self, f: F) -> Result<R, crate::error::ProgramError>
676    where
677        F: FnOnce(&T) -> Result<R, crate::error::ProgramError>,
678    {
679        let account = self.load()?;
680        f(&*account)
681    }
682
683    /// Borrow the account as another concrete layout in the same interface set.
684    ///
685    /// This is useful for marker interface accounts whose validation accepts a
686    /// bounded set of compatible layouts. The associated-type equality keeps a
687    /// caller from accidentally loading a layout governed by a different owner
688    /// set.
689    #[inline(always)]
690    pub fn load_as<U>(&self) -> Result<crate::borrow::Ref<'_, U>, crate::error::ProgramError>
691    where
692        U: InterfaceAccountLayout<Interface = <T as InterfaceAccountLayout>::Interface>,
693    {
694        self.inner.load_cross_program::<U>()
695    }
696
697    /// Friendly alias for [`Self::load_as`].
698    #[inline(always)]
699    pub fn get_as<U>(&self) -> Result<crate::borrow::Ref<'_, U>, crate::error::ProgramError>
700    where
701        U: InterfaceAccountLayout<Interface = <T as InterfaceAccountLayout>::Interface>,
702    {
703        self.load_as::<U>()
704    }
705
706    /// Borrow another concrete interface layout for the duration of a closure.
707    #[inline]
708    pub fn with_as<U, R, F>(&self, f: F) -> Result<R, crate::error::ProgramError>
709    where
710        U: InterfaceAccountLayout<Interface = <T as InterfaceAccountLayout>::Interface>,
711        F: FnOnce(&U) -> Result<R, crate::error::ProgramError>,
712    {
713        let account = self.load_as::<U>()?;
714        f(&*account)
715    }
716
717    /// Whether the current bytes match another concrete layout in the same
718    /// interface set.
719    #[inline(always)]
720    pub fn is<U>(&self) -> bool
721    where
722        U: InterfaceAccountLayout<Interface = <T as InterfaceAccountLayout>::Interface>,
723    {
724        self.inner
725            .layout_info()
726            .is_some_and(|info| info.matches::<U>())
727    }
728
729    /// Resolve a marker interface account to one of its concrete variants.
730    #[inline(always)]
731    pub fn resolve(&self) -> Result<T::Resolved<'_>, crate::error::ProgramError>
732    where
733        T: InterfaceAccountResolve,
734    {
735        T::resolve(self.inner)
736    }
737}
738
739impl<'info, T: InterfaceAccountLayout> core::ops::Deref for InterfaceAccount<'info, T> {
740    type Target = AccountView;
741
742    #[inline(always)]
743    fn deref(&self) -> &AccountView {
744        self.inner
745    }
746}
747
748/// Marker trait for a compile-time-known program ID.
749///
750/// Callers wire programs into Hopper contexts by implementing this on
751/// a unit struct; the canonical names (`SystemId`, `TokenId`,
752/// `AssociatedTokenId`, `Token2022Id`) are provided below for the
753/// Solana programs most Hopper programs depend on.
754pub trait ProgramId: 'static {
755    const ID: Address;
756}
757
758/// Solana System Program.
759pub struct SystemId;
760impl ProgramId for SystemId {
761    const ID: Address = Address::new_from_array([0u8; 32]);
762}
763
764#[cfg(test)]
765mod tests {
766    use super::*;
767
768    #[test]
769    fn signer_wrapper_is_pointer_sized_zero_cost() {
770        // `#[repr(transparent)]` guarantees the wrapper has the same
771        // ABI as `&AccountView`. This test is a compile-time
772        // assertion via `size_of`.
773        assert_eq!(
774            core::mem::size_of::<Signer<'static>>(),
775            core::mem::size_of::<&'static AccountView>()
776        );
777    }
778
779    #[test]
780    fn system_program_id_is_all_zero() {
781        let sys = SystemId::ID;
782        assert_eq!(sys.as_array(), &[0u8; 32]);
783    }
784}
785
786#[cfg(all(test, feature = "hopper-native-backend"))]
787mod resolver_tests {
788    use super::*;
789    use crate::layout::{HopperHeader, LayoutContract};
790
791    use hopper_native::{
792        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
793    };
794
795    const PROGRAM_A: Address = Address::new_from_array([0xA1; 32]);
796    const PROGRAM_B: Address = Address::new_from_array([0xB2; 32]);
797    const OTHER_PROGRAM: Address = Address::new_from_array([0xCC; 32]);
798
799    struct VaultPrograms;
800    impl InterfaceSpec for VaultPrograms {
801        const IDS: &'static [Address] = &[PROGRAM_A, PROGRAM_B];
802    }
803
804    #[repr(C)]
805    #[derive(Clone, Copy, Debug, Default)]
806    struct VaultV1 {
807        balance: u64,
808    }
809
810    impl crate::field_map::FieldMap for VaultV1 {
811        const FIELDS: &'static [crate::field_map::FieldInfo] = &[crate::field_map::FieldInfo::new(
812            "balance",
813            HopperHeader::SIZE,
814            8,
815        )];
816    }
817
818    impl LayoutContract for VaultV1 {
819        const DISC: u8 = 11;
820        const VERSION: u8 = 1;
821        const LAYOUT_ID: [u8; 8] = [0x11; 8];
822        const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
823    }
824
825    impl InterfaceAccountLayout for VaultV1 {
826        type Interface = VaultPrograms;
827    }
828
829    #[repr(C)]
830    #[derive(Clone, Copy, Debug, Default)]
831    struct VaultV2 {
832        balance: u64,
833        bump: u64,
834    }
835
836    impl crate::field_map::FieldMap for VaultV2 {
837        const FIELDS: &'static [crate::field_map::FieldInfo] = &[
838            crate::field_map::FieldInfo::new("balance", HopperHeader::SIZE, 8),
839            crate::field_map::FieldInfo::new("bump", HopperHeader::SIZE + 8, 8),
840        ];
841    }
842
843    impl LayoutContract for VaultV2 {
844        const DISC: u8 = 12;
845        const VERSION: u8 = 2;
846        const LAYOUT_ID: [u8; 8] = [0x22; 8];
847        const SIZE: usize = HopperHeader::SIZE + core::mem::size_of::<Self>();
848    }
849
850    impl InterfaceAccountLayout for VaultV2 {
851        type Interface = VaultPrograms;
852    }
853
854    #[derive(Clone, Copy, Debug, Default)]
855    struct AnyVault;
856
857    impl crate::field_map::FieldMap for AnyVault {
858        const FIELDS: &'static [crate::field_map::FieldInfo] = &[];
859    }
860
861    impl LayoutContract for AnyVault {
862        const DISC: u8 = 0;
863        const VERSION: u8 = 0;
864        const LAYOUT_ID: [u8; 8] = [0; 8];
865        const SIZE: usize = HopperHeader::SIZE;
866    }
867
868    impl InterfaceAccountLayout for AnyVault {
869        type Interface = VaultPrograms;
870
871        fn validate_interface_account(
872            view: &AccountView,
873        ) -> Result<(), crate::error::ProgramError> {
874            let data = view.try_borrow()?;
875            if VaultV1::validate_header(&data).is_ok() || VaultV2::validate_header(&data).is_ok() {
876                Ok(())
877            } else {
878                Err(crate::error::ProgramError::InvalidAccountData)
879            }
880        }
881    }
882
883    enum ResolvedVault<'a> {
884        V1(crate::borrow::Ref<'a, VaultV1>),
885        V2(crate::borrow::Ref<'a, VaultV2>),
886    }
887
888    impl InterfaceAccountResolve for AnyVault {
889        type Resolved<'a> = ResolvedVault<'a>;
890
891        fn resolve<'a>(
892            view: &'a AccountView,
893        ) -> Result<Self::Resolved<'a>, crate::error::ProgramError> {
894            let info = view
895                .layout_info()
896                .ok_or(crate::error::ProgramError::AccountDataTooSmall)?;
897            if info.matches::<VaultV1>() {
898                return Ok(ResolvedVault::V1(view.load_cross_program::<VaultV1>()?));
899            }
900            if info.matches::<VaultV2>() {
901                return Ok(ResolvedVault::V2(view.load_cross_program::<VaultV2>()?));
902            }
903            Err(crate::error::ProgramError::InvalidAccountData)
904        }
905    }
906
907    fn make_account(total_data_len: usize, owner: Address) -> (std::vec::Vec<u8>, AccountView) {
908        let mut backing = std::vec![0u8; RuntimeAccount::SIZE + total_data_len];
909        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
910        // SAFETY: The test owns `backing`, writes one RuntimeAccount header,
911        // and keeps the buffer alive for the returned AccountView.
912        unsafe {
913            raw.write(RuntimeAccount {
914                borrow_state: NOT_BORROWED,
915                is_signer: 1,
916                is_writable: 1,
917                executable: 0,
918                resize_delta: 0,
919                address: NativeAddress::new_from_array([0x44; 32]),
920                owner: NativeAddress::new_from_array(*owner.as_array()),
921                lamports: 42,
922                data_len: total_data_len as u64,
923            });
924        }
925        // SAFETY: `raw` points to the RuntimeAccount header initialized above.
926        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
927        (backing, AccountView::from_backend(backend))
928    }
929
930    #[test]
931    fn interface_account_resolves_bounded_layout_variants() {
932        let (_v1_backing, v1_account) = make_account(VaultV1::SIZE, PROGRAM_B);
933        {
934            let mut data = v1_account.try_borrow_mut().unwrap();
935            crate::layout::init_header::<VaultV1>(&mut data).unwrap();
936            data[HopperHeader::SIZE..HopperHeader::SIZE + 8].copy_from_slice(&300u64.to_le_bytes());
937        }
938
939        let v1_vault = InterfaceAccount::<AnyVault>::try_new(&v1_account).unwrap();
940        match v1_vault.resolve().unwrap() {
941            ResolvedVault::V1(v1) => {
942                assert_eq!(v1.balance, 300);
943            }
944            ResolvedVault::V2(_) => panic!("expected v1"),
945        }
946
947        let (_backing, account) = make_account(VaultV2::SIZE, PROGRAM_A);
948        {
949            let mut data = account.try_borrow_mut().unwrap();
950            crate::layout::init_header::<VaultV2>(&mut data).unwrap();
951            data[HopperHeader::SIZE..HopperHeader::SIZE + 8].copy_from_slice(&700u64.to_le_bytes());
952            data[HopperHeader::SIZE + 8..HopperHeader::SIZE + 16]
953                .copy_from_slice(&9u64.to_le_bytes());
954        }
955
956        let vault = InterfaceAccount::<AnyVault>::try_new(&account).unwrap();
957        assert!(vault.is::<VaultV2>());
958        assert!(!vault.is::<VaultV1>());
959
960        match vault.resolve().unwrap() {
961            ResolvedVault::V2(v2) => {
962                assert_eq!(v2.balance, 700);
963                assert_eq!(v2.bump, 9);
964            }
965            ResolvedVault::V1(_) => panic!("expected v2"),
966        }
967
968        let v2 = vault.load_as::<VaultV2>().unwrap();
969        assert_eq!(v2.balance, 700);
970        assert!(vault.get_as::<VaultV1>().is_err());
971    }
972
973    #[test]
974    fn interface_account_resolver_keeps_owner_and_layout_checks() {
975        let (_wrong_owner_backing, wrong_owner) = make_account(VaultV1::SIZE, OTHER_PROGRAM);
976        {
977            let mut data = wrong_owner.try_borrow_mut().unwrap();
978            crate::layout::init_header::<VaultV1>(&mut data).unwrap();
979        }
980        let wrong_owner_result = InterfaceAccount::<AnyVault>::try_new(&wrong_owner);
981        assert!(matches!(
982            wrong_owner_result,
983            Err(crate::error::ProgramError::IncorrectProgramId)
984        ));
985
986        let (_bad_layout_backing, bad_layout) = make_account(VaultV1::SIZE, PROGRAM_B);
987        {
988            let mut data = bad_layout.try_borrow_mut().unwrap();
989            crate::layout::write_header(&mut data, 99, 1, &[0x99; 8]).unwrap();
990        }
991        let bad_layout_result = InterfaceAccount::<AnyVault>::try_new(&bad_layout);
992        assert!(matches!(
993            bad_layout_result,
994            Err(crate::error::ProgramError::InvalidAccountData)
995        ));
996    }
997}