Skip to main content

hopper_core/frame/
phase.rs

1//! Phased execution builder for the Frame.
2//!
3//! Hopper's signature feature: typestate-driven phased execution that
4//! enforces correct ordering at compile time.
5//!
6//! ```text
7//! frame.resolve(accounts)?
8//!      .validate(|ctx| { ... })?
9//!      .execute(|ctx| { ... })?
10//! ```
11//!
12//! The typestate pattern means:
13//! - You cannot call `.execute()` before `.validate()`
14//! - You cannot call `.validate()` before `.resolve()`
15//! - Each transition is a zero-cost abstraction at runtime
16//!
17//! ## Phase Model
18//!
19//! ```text
20//! Unresolved -> Resolved -> Validated -> Executed
21//! ```
22
23use hopper_runtime::{error::ProgramError, AccountView, Address, ProgramResult, Ref, RefMut};
24
25// -- Phase Marker Types (zero-sized, compile-time only) --------------
26
27/// Phase: accounts not yet resolved.
28pub struct Unresolved;
29/// Phase: accounts resolved and typed.
30pub struct Resolved;
31/// Phase: validation passed.
32pub struct Validated;
33/// Phase: execution complete.
34pub struct Executed;
35
36// -- Phased Frame ----------------------------------------------------
37
38/// A phased execution context that enforces ordering via type state.
39///
40/// `P` is the current phase -- a zero-sized marker type.
41/// The frame itself carries no per-phase overhead at runtime;
42/// phase transitions are compile-time checked.
43pub struct PhasedFrame<'a, P> {
44    program_id: &'a Address,
45    accounts: &'a [AccountView],
46    ix_data: &'a [u8],
47    mutable_borrows: u64,
48    _phase: core::marker::PhantomData<P>,
49}
50
51impl<'a> PhasedFrame<'a, Unresolved> {
52    /// Create a new phased frame in the `Unresolved` state.
53    #[inline(always)]
54    pub fn new(
55        program_id: &'a Address,
56        accounts: &'a [AccountView],
57        ix_data: &'a [u8],
58    ) -> Result<Self, ProgramError> {
59        if accounts.len() > crate::frame::MAX_FRAME_ACCOUNTS {
60            return Err(ProgramError::InvalidArgument);
61        }
62        Ok(Self {
63            program_id,
64            accounts,
65            ix_data,
66            mutable_borrows: 0,
67            _phase: core::marker::PhantomData,
68        })
69    }
70
71    /// Resolve accounts -- validate account count and transition to `Resolved`.
72    ///
73    /// The closure receives the accounts slice and program_id, allowing
74    /// the caller to parse/index accounts into a typed struct.
75    ///
76    /// ```ignore
77    /// let resolved = frame.resolve(|accounts, program_id| {
78    ///     Ok(MyAccounts {
79    ///         payer: &accounts[0],
80    ///         vault: &accounts[1],
81    ///     })
82    /// })?;
83    /// ```
84    #[inline]
85    pub fn resolve<T, F>(
86        self,
87        min_accounts: usize,
88        f: F,
89    ) -> Result<ResolvedFrame<'a, T>, ProgramError>
90    where
91        F: FnOnce(&'a [AccountView], &'a Address) -> Result<T, ProgramError>,
92    {
93        if self.accounts.len() < min_accounts {
94            return Err(ProgramError::NotEnoughAccountKeys);
95        }
96        let resolved = f(self.accounts, self.program_id)?;
97        Ok(ResolvedFrame {
98            program_id: self.program_id,
99            accounts: self.accounts,
100            ix_data: self.ix_data,
101            mutable_borrows: self.mutable_borrows,
102            resolved,
103        })
104    }
105}
106
107/// A frame that has been resolved with typed account references.
108///
109/// `T` is the user's account struct (e.g., `SwapAccounts<'a>`).
110pub struct ResolvedFrame<'a, T> {
111    pub(crate) program_id: &'a Address,
112    pub(crate) accounts: &'a [AccountView],
113    pub(crate) ix_data: &'a [u8],
114    pub(crate) mutable_borrows: u64,
115    pub(crate) resolved: T,
116}
117
118impl<'a, T> ResolvedFrame<'a, T> {
119    /// Program ID.
120    #[inline(always)]
121    pub fn program_id(&self) -> &Address {
122        self.program_id
123    }
124
125    /// Instruction data.
126    #[inline(always)]
127    pub fn ix_data(&self) -> &[u8] {
128        self.ix_data
129    }
130
131    /// Access the resolved accounts.
132    #[inline(always)]
133    pub fn accounts(&self) -> &T {
134        &self.resolved
135    }
136
137    /// Validate constraints and transition to `ValidatedFrame`.
138    ///
139    /// The closure receives the resolved accounts for validation. It should
140    /// call `check_*` functions and return `Ok(())` on success.
141    ///
142    /// ```ignore
143    /// let validated = resolved.validate(|ctx| {
144    ///     check_signer(ctx.payer)?;
145    ///     check_owner(ctx.vault, program_id)?;
146    ///     Ok(())
147    /// })?;
148    /// ```
149    #[inline]
150    pub fn validate<F>(self, f: F) -> Result<ValidatedFrame<'a, T>, ProgramError>
151    where
152        F: FnOnce(&T, &Address) -> ProgramResult,
153    {
154        f(&self.resolved, self.program_id)?;
155        Ok(ValidatedFrame {
156            program_id: self.program_id,
157            accounts: self.accounts,
158            ix_data: self.ix_data,
159            mutable_borrows: self.mutable_borrows,
160            resolved: self.resolved,
161        })
162    }
163}
164
165/// A frame whose accounts have been validated.
166pub struct ValidatedFrame<'a, T> {
167    pub(crate) program_id: &'a Address,
168    pub(crate) accounts: &'a [AccountView],
169    pub(crate) ix_data: &'a [u8],
170    pub(crate) mutable_borrows: u64,
171    pub(crate) resolved: T,
172}
173
174impl<'a, T> ValidatedFrame<'a, T> {
175    /// Program ID.
176    #[inline(always)]
177    pub fn program_id(&self) -> &Address {
178        self.program_id
179    }
180
181    /// Instruction data.
182    #[inline(always)]
183    pub fn ix_data(&self) -> &[u8] {
184        self.ix_data
185    }
186
187    /// Access the resolved and validated accounts.
188    #[inline(always)]
189    pub fn accounts(&self) -> &T {
190        &self.resolved
191    }
192
193    /// Execute the instruction logic.
194    ///
195    /// The closure receives an `ExecutionContext` with mutable access to
196    /// the validated accounts and mutable borrow tracking.
197    ///
198    /// ```ignore
199    /// validated.execute(|ctx| {
200    ///     let vault_data = ctx.borrow_mut(1)?;
201    ///     // ... mutate state ...
202    ///     Ok(())
203    /// })?;
204    /// ```
205    #[inline]
206    pub fn execute<R, F>(mut self, f: F) -> Result<R, ProgramError>
207    where
208        F: FnOnce(&mut ExecutionContext<'a, '_, T>) -> Result<R, ProgramError>,
209    {
210        let mut ctx = ExecutionContext {
211            program_id: self.program_id,
212            accounts: self.accounts,
213            ix_data: self.ix_data,
214            mutable_borrows: &mut self.mutable_borrows,
215            resolved: &self.resolved,
216        };
217        f(&mut ctx)
218    }
219}
220
221/// Mutable execution context available during the Execute phase.
222pub struct ExecutionContext<'a, 'f, T> {
223    pub(crate) program_id: &'a Address,
224    pub(crate) accounts: &'a [AccountView],
225    pub(crate) ix_data: &'a [u8],
226    pub(crate) mutable_borrows: &'f mut u64,
227    pub(crate) resolved: &'f T,
228}
229
230impl<'a, 'f, T> ExecutionContext<'a, 'f, T> {
231    /// Program ID.
232    #[inline(always)]
233    pub fn program_id(&self) -> &'a Address {
234        self.program_id
235    }
236
237    /// Instruction data.
238    #[inline(always)]
239    pub fn ix_data(&self) -> &'a [u8] {
240        self.ix_data
241    }
242
243    /// Resolved accounts.
244    #[inline(always)]
245    pub fn resolved(&self) -> &T {
246        self.resolved
247    }
248
249    /// Borrow account data mutably with runtime aliasing protection.
250    #[inline]
251    pub fn borrow_mut(&mut self, index: usize) -> Result<RefMut<'a, [u8]>, ProgramError> {
252        if index >= self.accounts.len() {
253            return Err(ProgramError::NotEnoughAccountKeys);
254        }
255        let bit = 1u64 << (index as u32);
256        if *self.mutable_borrows & bit != 0 {
257            return Err(ProgramError::AccountBorrowFailed);
258        }
259        *self.mutable_borrows |= bit;
260        // SAFETY: Borrow tracking prevents aliasing. Caller proved validation.
261        match self.accounts[index].try_borrow_mut() {
262            Ok(data) => Ok(data),
263            Err(error) => {
264                *self.mutable_borrows &= !bit;
265                Err(error)
266            }
267        }
268    }
269
270    /// Borrow account data immutably.
271    #[inline(always)]
272    pub fn borrow(&self, index: usize) -> Result<Ref<'a, [u8]>, ProgramError> {
273        if index >= self.accounts.len() {
274            return Err(ProgramError::NotEnoughAccountKeys);
275        }
276        // SAFETY: Immutable borrow does not conflict.
277        self.accounts[index].try_borrow()
278    }
279
280    /// Get raw AccountView by index.
281    #[inline(always)]
282    pub fn account(&self, index: usize) -> Result<&'a AccountView, ProgramError> {
283        self.accounts
284            .get(index)
285            .ok_or(ProgramError::NotEnoughAccountKeys)
286    }
287}