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 {
45    inner: AccountView,
46}
47
48impl SignerView {
49    /// Validate that the account is a signer and return a capability token.
50    #[inline(always)]
51    pub fn validate(view: AccountView) -> 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 {
62        &self.inner
63    }
64
65    /// Consume and return the inner `AccountView`.
66    #[inline(always)]
67    pub fn into_view(self) -> AccountView {
68        self.inner
69    }
70}
71
72impl core::ops::Deref for SignerView {
73    type Target = AccountView;
74
75    #[inline(always)]
76    fn deref(&self) -> &AccountView {
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 {
89    inner: AccountView,
90}
91
92impl WritableView {
93    /// Validate that the account is writable and return a capability token.
94    #[inline(always)]
95    pub fn validate(view: AccountView) -> 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 {
106        &self.inner
107    }
108
109    /// Consume and return the inner `AccountView`.
110    #[inline(always)]
111    pub fn into_view(self) -> AccountView {
112        self.inner
113    }
114}
115
116impl core::ops::Deref for WritableView {
117    type Target = AccountView;
118
119    #[inline(always)]
120    fn deref(&self) -> &AccountView {
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 {
135    inner: AccountView,
136}
137
138impl MutableView {
139    /// Validate that the account is both a signer and writable.
140    #[inline(always)]
141    pub fn validate(view: AccountView) -> 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 {
154        &self.inner
155    }
156
157    /// Consume and return the inner `AccountView`.
158    #[inline(always)]
159    pub fn into_view(self) -> AccountView {
160        self.inner
161    }
162
163    /// Upcast to `SignerView` (free -- MutableView implies signer).
164    #[inline(always)]
165    pub fn as_signer(&self) -> SignerView {
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 {
175        // SAFETY: MutableView guarantees is_writable.
176        WritableView {
177            inner: self.inner.clone(),
178        }
179    }
180}
181
182impl core::ops::Deref for MutableView {
183    type Target = AccountView;
184
185    #[inline(always)]
186    fn deref(&self) -> &AccountView {
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 {
200    inner: AccountView,
201}
202
203impl OwnedView {
204    /// Validate that the account is owned by `expected_owner`.
205    #[inline(always)]
206    pub fn validate(view: AccountView, expected_owner: &Address) -> Result<Self, ProgramError> {
207        if view.owned_by(expected_owner) {
208            Ok(Self { inner: view })
209        } else {
210            Err(ProgramError::IncorrectProgramId)
211        }
212    }
213
214    /// Access the underlying `AccountView`.
215    #[inline(always)]
216    pub fn as_view(&self) -> &AccountView {
217        &self.inner
218    }
219
220    /// Consume and return the inner `AccountView`.
221    #[inline(always)]
222    pub fn into_view(self) -> AccountView {
223        self.inner
224    }
225}
226
227impl core::ops::Deref for OwnedView {
228    type Target = AccountView;
229
230    #[inline(always)]
231    fn deref(&self) -> &AccountView {
232        &self.inner
233    }
234}
235
236// ── ReadonlyView ─────────────────────────────────────────────────────
237
238/// An `AccountView` proven to be a non-signer, non-writable read-only
239/// account. Useful for cross-program reads where you explicitly want
240/// to prevent accidental mutation attempts.
241#[repr(transparent)]
242#[derive(Clone, PartialEq, Eq)]
243pub struct ReadonlyView {
244    inner: AccountView,
245}
246
247impl ReadonlyView {
248    /// Validate that the account is neither a signer nor writable.
249    #[inline(always)]
250    pub fn validate(view: AccountView) -> Result<Self, ProgramError> {
251        // A "readonly" account in Solana's model is one that the
252        // transaction declared as non-writable. We don't require
253        // non-signer because some read-only lookups still need signer
254        // proof. Instead we just check non-writable.
255        if view.is_writable() {
256            // Account is writable -- caller probably mixed up their types.
257            return Err(ProgramError::InvalidArgument);
258        }
259        Ok(Self { inner: view })
260    }
261
262    /// Access the underlying `AccountView`.
263    #[inline(always)]
264    pub fn as_view(&self) -> &AccountView {
265        &self.inner
266    }
267
268    /// Consume and return the inner `AccountView`.
269    #[inline(always)]
270    pub fn into_view(self) -> AccountView {
271        self.inner
272    }
273}
274
275impl core::ops::Deref for ReadonlyView {
276    type Target = AccountView;
277
278    #[inline(always)]
279    fn deref(&self) -> &AccountView {
280        &self.inner
281    }
282}
283
284// ── ExecutableView ───────────────────────────────────────────────────
285
286/// An `AccountView` proven to contain an executable program.
287///
288/// Used when passing program accounts for CPI -- proves the account
289/// actually contains a program, preventing CPI to data accounts.
290#[repr(transparent)]
291#[derive(Clone, PartialEq, Eq)]
292pub struct ExecutableView {
293    inner: AccountView,
294}
295
296impl ExecutableView {
297    /// Validate that the account is executable.
298    #[inline(always)]
299    pub fn validate(view: AccountView) -> Result<Self, ProgramError> {
300        if view.executable() {
301            Ok(Self { inner: view })
302        } else {
303            Err(ProgramError::InvalidArgument)
304        }
305    }
306
307    /// Access the underlying `AccountView`.
308    #[inline(always)]
309    pub fn as_view(&self) -> &AccountView {
310        &self.inner
311    }
312
313    /// Consume and return the inner `AccountView`.
314    #[inline(always)]
315    pub fn into_view(self) -> AccountView {
316        self.inner
317    }
318}
319
320impl core::ops::Deref for ExecutableView {
321    type Target = AccountView;
322
323    #[inline(always)]
324    fn deref(&self) -> &AccountView {
325        &self.inner
326    }
327}
328
329// ── Capability Composition via LazyContext ────────────────────────────
330
331impl crate::lazy::LazyContext {
332    /// Parse the next account as a proven signer.
333    #[inline]
334    pub fn next_validated_signer(&mut self) -> Result<SignerView, ProgramError> {
335        let acct = self.next_account()?;
336        SignerView::validate(acct)
337    }
338
339    /// Parse the next account as a proven writable.
340    #[inline]
341    pub fn next_validated_writable(&mut self) -> Result<WritableView, ProgramError> {
342        let acct = self.next_account()?;
343        WritableView::validate(acct)
344    }
345
346    /// Parse the next account as a proven mutable (signer + writable).
347    #[inline]
348    pub fn next_validated_mutable(&mut self) -> Result<MutableView, ProgramError> {
349        let acct = self.next_account()?;
350        MutableView::validate(acct)
351    }
352
353    /// Parse the next account as a proven program-owned account.
354    #[inline]
355    pub fn next_validated_owned(&mut self, owner: &Address) -> Result<OwnedView, ProgramError> {
356        let acct = self.next_account()?;
357        OwnedView::validate(acct, owner)
358    }
359
360    /// Parse the next account as a proven executable program.
361    #[inline]
362    pub fn next_validated_executable(&mut self) -> Result<ExecutableView, ProgramError> {
363        let acct = self.next_account()?;
364        ExecutableView::validate(acct)
365    }
366}