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