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}