Skip to main content

hopper_native/
capability.rs

1//! Compile-time account capability types.
2//!
3//! Instead of sprinkling `require_signer()` / `require_writable()` calls
4//! throughout business logic, Hopper elevates account roles to the type
5//! system. A `SignerView` proves at compile time that the signer check
6//! happened. Functions that need a signer take `SignerView` -- zero
7//! runtime cost after the single boundary check.
8//!
9//! This pattern has no equivalent in pinocchio, Anchor, Steel, or any
10//! other Solana framework. Anchor's `Signer<'info>` is a macro-generated
11//! wrapper that re-checks at runtime. Hopper's capability types are
12//! zero-size wrappers that PROVE the check already happened.
13//!
14//! # Usage
15//!
16//! ```ignore
17//! use hopper_native::capability::{SignerView, WritableView, MutableView};
18//!
19//! fn deposit(
20//!     payer: MutableView,     // proven: is_signer + is_writable
21//!     vault: WritableView,    // proven: is_writable
22//!     amount: u64,
23//! ) -> ProgramResult {
24//!     // No runtime checks needed -- the types guarantee the properties.
25//!     let lamports = payer.lamports();
26//!     // ...
27//!     Ok(())
28//! }
29//! ```
30
31use crate::account_view::AccountView;
32use crate::address::Address;
33use crate::error::ProgramError;
34
35// ── SignerView ───────────────────────────────────────────────────────
36
37/// An `AccountView` that has been proven to be a transaction signer.
38///
39/// Constructed only through `SignerView::validate()`, which performs the
40/// signer check exactly once. All downstream code can rely on the type
41/// to guarantee the property without re-checking.
42#[repr(transparent)]
43#[derive(Clone, PartialEq, Eq)]
44pub struct SignerView<'info> {
45    inner: AccountView<'info>,
46}
47
48impl<'info> SignerView<'info> {
49    /// Validate that the account is a signer and return a capability token.
50    #[inline(always)]
51    pub fn validate(view: AccountView<'info>) -> Result<Self, ProgramError> {
52        if view.is_signer() {
53            Ok(Self { inner: view })
54        } else {
55            Err(ProgramError::MissingRequiredSignature)
56        }
57    }
58
59    /// Access the underlying `AccountView`.
60    #[inline(always)]
61    pub fn as_view(&self) -> &AccountView<'info> {
62        &self.inner
63    }
64
65    /// Consume and return the inner `AccountView`.
66    #[inline(always)]
67    pub fn into_view(self) -> AccountView<'info> {
68        self.inner
69    }
70}
71
72impl<'info> core::ops::Deref for SignerView<'info> {
73    type Target = AccountView<'info>;
74
75    #[inline(always)]
76    fn deref(&self) -> &AccountView<'info> {
77        &self.inner
78    }
79}
80
81// ── WritableView ─────────────────────────────────────────────────────
82
83/// An `AccountView` that has been proven to be writable.
84///
85/// Guarantees that `is_writable() == true` without re-checking.
86#[repr(transparent)]
87#[derive(Clone, PartialEq, Eq)]
88pub struct WritableView<'info> {
89    inner: AccountView<'info>,
90}
91
92impl<'info> WritableView<'info> {
93    /// Validate that the account is writable and return a capability token.
94    #[inline(always)]
95    pub fn validate(view: AccountView<'info>) -> Result<Self, ProgramError> {
96        if view.is_writable() {
97            Ok(Self { inner: view })
98        } else {
99            Err(ProgramError::Immutable)
100        }
101    }
102
103    /// Access the underlying `AccountView`.
104    #[inline(always)]
105    pub fn as_view(&self) -> &AccountView<'info> {
106        &self.inner
107    }
108
109    /// Consume and return the inner `AccountView`.
110    #[inline(always)]
111    pub fn into_view(self) -> AccountView<'info> {
112        self.inner
113    }
114}
115
116impl<'info> core::ops::Deref for WritableView<'info> {
117    type Target = AccountView<'info>;
118
119    #[inline(always)]
120    fn deref(&self) -> &AccountView<'info> {
121        &self.inner
122    }
123}
124
125// ── MutableView ──────────────────────────────────────────────────────
126
127/// An `AccountView` that has been proven to be BOTH a signer AND writable.
128///
129/// This is the "payer" pattern: the account that signs and pays for the
130/// transaction. The check happens once; all downstream code gets both
131/// guarantees via the type.
132#[repr(transparent)]
133#[derive(Clone, PartialEq, Eq)]
134pub struct MutableView<'info> {
135    inner: AccountView<'info>,
136}
137
138impl<'info> MutableView<'info> {
139    /// Validate that the account is both a signer and writable.
140    #[inline(always)]
141    pub fn validate(view: AccountView<'info>) -> Result<Self, ProgramError> {
142        if !view.is_signer() {
143            return Err(ProgramError::MissingRequiredSignature);
144        }
145        if !view.is_writable() {
146            return Err(ProgramError::Immutable);
147        }
148        Ok(Self { inner: view })
149    }
150
151    /// Access the underlying `AccountView`.
152    #[inline(always)]
153    pub fn as_view(&self) -> &AccountView<'info> {
154        &self.inner
155    }
156
157    /// Consume and return the inner `AccountView`.
158    #[inline(always)]
159    pub fn into_view(self) -> AccountView<'info> {
160        self.inner
161    }
162
163    /// Upcast to `SignerView` (free -- MutableView implies signer).
164    #[inline(always)]
165    pub fn as_signer(&self) -> SignerView<'info> {
166        // SAFETY: MutableView guarantees is_signer.
167        SignerView {
168            inner: self.inner.clone(),
169        }
170    }
171
172    /// Upcast to `WritableView` (free -- MutableView implies writable).
173    #[inline(always)]
174    pub fn as_writable(&self) -> WritableView<'info> {
175        // SAFETY: MutableView guarantees is_writable.
176        WritableView {
177            inner: self.inner.clone(),
178        }
179    }
180}
181
182impl<'info> core::ops::Deref for MutableView<'info> {
183    type Target = AccountView<'info>;
184
185    #[inline(always)]
186    fn deref(&self) -> &AccountView<'info> {
187        &self.inner
188    }
189}
190
191// ── OwnedView ────────────────────────────────────────────────────────
192
193/// An `AccountView` that has been proven to be owned by a specific program.
194///
195/// Prevents confused-deputy attacks: once validated, downstream code
196/// can trust the account data without re-checking ownership.
197#[repr(transparent)]
198#[derive(Clone, PartialEq, Eq)]
199pub struct OwnedView<'info> {
200    inner: AccountView<'info>,
201}
202
203impl<'info> OwnedView<'info> {
204    /// Validate that the account is owned by `expected_owner`.
205    #[inline(always)]
206    pub fn validate(
207        view: AccountView<'info>,
208        expected_owner: &Address,
209    ) -> Result<Self, ProgramError> {
210        if view.owned_by(expected_owner) {
211            Ok(Self { inner: view })
212        } else {
213            Err(ProgramError::IncorrectProgramId)
214        }
215    }
216
217    /// Access the underlying `AccountView`.
218    #[inline(always)]
219    pub fn as_view(&self) -> &AccountView<'info> {
220        &self.inner
221    }
222
223    /// Consume and return the inner `AccountView`.
224    #[inline(always)]
225    pub fn into_view(self) -> AccountView<'info> {
226        self.inner
227    }
228}
229
230impl<'info> core::ops::Deref for OwnedView<'info> {
231    type Target = AccountView<'info>;
232
233    #[inline(always)]
234    fn deref(&self) -> &AccountView<'info> {
235        &self.inner
236    }
237}
238
239// ── ReadonlyView ─────────────────────────────────────────────────────
240
241/// An `AccountView` proven to be a non-signer, non-writable read-only
242/// account. Useful for cross-program reads where you explicitly want
243/// to prevent accidental mutation attempts.
244#[repr(transparent)]
245#[derive(Clone, PartialEq, Eq)]
246pub struct ReadonlyView<'info> {
247    inner: AccountView<'info>,
248}
249
250impl<'info> ReadonlyView<'info> {
251    /// Validate that the account is neither a signer nor writable.
252    #[inline(always)]
253    pub fn validate(view: AccountView<'info>) -> Result<Self, ProgramError> {
254        // A "readonly" account in Solana's model is one that the
255        // transaction declared as non-writable. We don't require
256        // non-signer because some read-only lookups still need signer
257        // proof. Instead we just check non-writable.
258        if view.is_writable() {
259            // Account is writable -- caller probably mixed up their types.
260            return Err(ProgramError::InvalidArgument);
261        }
262        Ok(Self { inner: view })
263    }
264
265    /// Access the underlying `AccountView`.
266    #[inline(always)]
267    pub fn as_view(&self) -> &AccountView<'info> {
268        &self.inner
269    }
270
271    /// Consume and return the inner `AccountView`.
272    #[inline(always)]
273    pub fn into_view(self) -> AccountView<'info> {
274        self.inner
275    }
276}
277
278impl<'info> core::ops::Deref for ReadonlyView<'info> {
279    type Target = AccountView<'info>;
280
281    #[inline(always)]
282    fn deref(&self) -> &AccountView<'info> {
283        &self.inner
284    }
285}
286
287// ── ExecutableView ───────────────────────────────────────────────────
288
289/// An `AccountView` proven to contain an executable program.
290///
291/// Used when passing program accounts for CPI -- proves the account
292/// actually contains a program, preventing CPI to data accounts.
293#[repr(transparent)]
294#[derive(Clone, PartialEq, Eq)]
295pub struct ExecutableView<'info> {
296    inner: AccountView<'info>,
297}
298
299impl<'info> ExecutableView<'info> {
300    /// Validate that the account is executable.
301    #[inline(always)]
302    pub fn validate(view: AccountView<'info>) -> Result<Self, ProgramError> {
303        if view.executable() {
304            Ok(Self { inner: view })
305        } else {
306            Err(ProgramError::InvalidArgument)
307        }
308    }
309
310    /// Access the underlying `AccountView`.
311    #[inline(always)]
312    pub fn as_view(&self) -> &AccountView<'info> {
313        &self.inner
314    }
315
316    /// Consume and return the inner `AccountView`.
317    #[inline(always)]
318    pub fn into_view(self) -> AccountView<'info> {
319        self.inner
320    }
321}
322
323impl<'info> core::ops::Deref for ExecutableView<'info> {
324    type Target = AccountView<'info>;
325
326    #[inline(always)]
327    fn deref(&self) -> &AccountView<'info> {
328        &self.inner
329    }
330}
331
332// ── Capability Composition via LazyContext ────────────────────────────
333
334impl<'info> crate::lazy::LazyContext<'info> {
335    /// Parse the next account as a proven signer.
336    #[inline]
337    pub fn next_validated_signer(&mut self) -> Result<SignerView<'info>, ProgramError> {
338        let acct = self.next_account()?;
339        SignerView::validate(acct)
340    }
341
342    /// Parse the next account as a proven writable.
343    #[inline]
344    pub fn next_validated_writable(&mut self) -> Result<WritableView<'info>, ProgramError> {
345        let acct = self.next_account()?;
346        WritableView::validate(acct)
347    }
348
349    /// Parse the next account as a proven mutable (signer + writable).
350    #[inline]
351    pub fn next_validated_mutable(&mut self) -> Result<MutableView<'info>, ProgramError> {
352        let acct = self.next_account()?;
353        MutableView::validate(acct)
354    }
355
356    /// Parse the next account as a proven program-owned account.
357    #[inline]
358    pub fn next_validated_owned(
359        &mut self,
360        owner: &Address,
361    ) -> Result<OwnedView<'info>, ProgramError> {
362        let acct = self.next_account()?;
363        OwnedView::validate(acct, owner)
364    }
365
366    /// Parse the next account as a proven executable program.
367    #[inline]
368    pub fn next_validated_executable(&mut self) -> Result<ExecutableView<'info>, ProgramError> {
369        let acct = self.next_account()?;
370        ExecutableView::validate(acct)
371    }
372}