Skip to main content

hopper_runtime/
instruction.rs

1//! Hopper-owned CPI instruction types.
2//!
3//! These types form the instruction ABI for Hopper cross-program invocation.
4//! They are Hopper-owned and reference Hopper's `Address` and `AccountView`
5//! types, giving the framework full control over its public API surface.
6
7use crate::account::AccountView;
8use crate::address::Address;
9use crate::error::ProgramError;
10use core::marker::PhantomData;
11use core::mem::MaybeUninit;
12
13// ── InstructionAccount ───────────────────────────────────────────────
14
15/// Metadata for an account referenced in a CPI instruction.
16#[repr(C)]
17#[derive(Debug, Clone, Copy)]
18pub struct InstructionAccount<'a> {
19    /// Public key of the account.
20    pub address: &'a Address,
21    /// Whether the account should be writable.
22    pub is_writable: bool,
23    /// Whether the account should sign.
24    pub is_signer: bool,
25}
26
27impl<'a> InstructionAccount<'a> {
28    /// Construct with explicit flags.
29    #[inline(always)]
30    pub const fn new(address: &'a Address, is_writable: bool, is_signer: bool) -> Self {
31        Self {
32            address,
33            is_writable,
34            is_signer,
35        }
36    }
37
38    /// Read-only, non-signer.
39    #[inline(always)]
40    pub const fn readonly(address: &'a Address) -> Self {
41        Self {
42            address,
43            is_writable: false,
44            is_signer: false,
45        }
46    }
47
48    /// Writable, non-signer.
49    #[inline(always)]
50    pub const fn writable(address: &'a Address) -> Self {
51        Self {
52            address,
53            is_writable: true,
54            is_signer: false,
55        }
56    }
57
58    /// Read-only signer.
59    #[inline(always)]
60    pub const fn readonly_signer(address: &'a Address) -> Self {
61        Self {
62            address,
63            is_writable: false,
64            is_signer: true,
65        }
66    }
67
68    /// Writable signer.
69    #[inline(always)]
70    pub const fn writable_signer(address: &'a Address) -> Self {
71        Self {
72            address,
73            is_writable: true,
74            is_signer: true,
75        }
76    }
77}
78
79impl<'a> From<&'a AccountView<'a>> for InstructionAccount<'a> {
80    #[inline(always)]
81    fn from(view: &'a AccountView<'a>) -> Self {
82        Self {
83            address: view.address(),
84            is_writable: view.is_writable(),
85            is_signer: view.is_signer(),
86        }
87    }
88}
89
90/// Stored account metadata for governance/proposal-style arbitrary CPI.
91///
92/// The wire form is compact and owner-agnostic: a public key plus explicit
93/// signer/writable flags. It can be stored inside dynamic tails and converted
94/// to [`InstructionAccount`] without allocation when executing the proposal.
95#[repr(C)]
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct StoredAccountMeta {
98    /// Account public key.
99    pub pubkey: Address,
100    /// Bit 0 = signer, bit 1 = writable.
101    pub flags: u8,
102}
103
104impl StoredAccountMeta {
105    /// Signer flag bit.
106    pub const SIGNER: u8 = 0b0000_0001;
107    /// Writable flag bit.
108    pub const WRITABLE: u8 = 0b0000_0010;
109
110    /// Construct account metadata from explicit booleans.
111    #[inline(always)]
112    pub const fn new(pubkey: Address, is_signer: bool, is_writable: bool) -> Self {
113        let mut flags = 0u8;
114        if is_signer {
115            flags |= Self::SIGNER;
116        }
117        if is_writable {
118            flags |= Self::WRITABLE;
119        }
120        Self { pubkey, flags }
121    }
122
123    /// Read-only, non-signer account metadata.
124    #[inline(always)]
125    pub const fn readonly(pubkey: Address) -> Self {
126        Self::new(pubkey, false, false)
127    }
128
129    /// Writable, non-signer account metadata.
130    #[inline(always)]
131    pub const fn writable(pubkey: Address) -> Self {
132        Self::new(pubkey, false, true)
133    }
134
135    /// Read-only signer account metadata.
136    #[inline(always)]
137    pub const fn readonly_signer(pubkey: Address) -> Self {
138        Self::new(pubkey, true, false)
139    }
140
141    /// Writable signer account metadata.
142    #[inline(always)]
143    pub const fn writable_signer(pubkey: Address) -> Self {
144        Self::new(pubkey, true, true)
145    }
146
147    /// Whether this account must sign.
148    #[inline(always)]
149    pub const fn is_signer(&self) -> bool {
150        self.flags & Self::SIGNER != 0
151    }
152
153    /// Whether this account must be writable.
154    #[inline(always)]
155    pub const fn is_writable(&self) -> bool {
156        self.flags & Self::WRITABLE != 0
157    }
158
159    /// Convert this stored meta to CPI metadata.
160    #[inline(always)]
161    pub fn to_instruction_account(&self) -> InstructionAccount<'_> {
162        InstructionAccount::new(&self.pubkey, self.is_writable(), self.is_signer())
163    }
164}
165
166/// Borrowed stored instruction payload for proposal/governance execution.
167#[derive(Debug, Clone, Copy)]
168pub struct StoredInstruction<'a> {
169    /// Program to invoke.
170    pub program_id: Address,
171    /// Stored account metas in CPI order.
172    pub account_metas: &'a [StoredAccountMeta],
173    /// Stored instruction data.
174    pub instruction_data: &'a [u8],
175}
176
177impl<'a> StoredInstruction<'a> {
178    /// Construct a stored instruction with CPI account-count bounds.
179    #[inline]
180    pub fn new(
181        program_id: Address,
182        account_metas: &'a [StoredAccountMeta],
183        instruction_data: &'a [u8],
184    ) -> Result<Self, ProgramError> {
185        if account_metas.len() > crate::cpi::MAX_CPI_ACCOUNTS {
186            return Err(ProgramError::InvalidArgument);
187        }
188        Ok(Self {
189            program_id,
190            account_metas,
191            instruction_data,
192        })
193    }
194
195    /// Number of account metas in this stored instruction.
196    #[inline(always)]
197    pub const fn account_count(&self) -> usize {
198        self.account_metas.len()
199    }
200
201    /// Convert stored metas into a caller-provided CPI account buffer.
202    #[inline]
203    pub fn write_instruction_accounts<const N: usize>(
204        &'a self,
205        out: &'a mut [MaybeUninit<InstructionAccount<'a>>; N],
206    ) -> Result<&'a [InstructionAccount<'a>], ProgramError> {
207        if self.account_metas.len() > N {
208            return Err(ProgramError::InvalidArgument);
209        }
210        let mut index = 0;
211        while index < self.account_metas.len() {
212            out[index].write(self.account_metas[index].to_instruction_account());
213            index += 1;
214        }
215        // SAFETY: Elements `0..account_metas.len()` were fully initialized by
216        // the loop above, and we expose exactly that initialized prefix.
217        Ok(unsafe {
218            core::slice::from_raw_parts(
219                out.as_ptr() as *const InstructionAccount<'a>,
220                self.account_metas.len(),
221            )
222        })
223    }
224
225    /// Build an [`InstructionView`] using a caller-provided account buffer.
226    #[inline]
227    pub fn to_instruction_view<const N: usize>(
228        &'a self,
229        out: &'a mut [MaybeUninit<InstructionAccount<'a>>; N],
230    ) -> Result<InstructionView<'a, 'a, 'a, 'a>, ProgramError> {
231        let accounts = self.write_instruction_accounts(out)?;
232        Ok(InstructionView {
233            program_id: &self.program_id,
234            data: self.instruction_data,
235            accounts,
236        })
237    }
238}
239
240// ── InstructionView ──────────────────────────────────────────────────
241
242/// A cross-program instruction to invoke.
243#[derive(Debug, Clone)]
244pub struct InstructionView<'a, 'b, 'c, 'd>
245where
246    'a: 'b,
247{
248    /// Program to call.
249    pub program_id: &'c Address,
250    /// Instruction data.
251    pub data: &'d [u8],
252    /// Account metadata.
253    pub accounts: &'b [InstructionAccount<'a>],
254}
255
256// ── CpiAccount ──────────────────────────────────────────────────────
257
258/// C-ABI account info passed to `sol_invoke_signed_c`.
259///
260/// This matches the Solana runtime's expected layout for CPI account infos.
261/// This requires direct pointer access into Hopper's runtime account memory.
262#[repr(C)]
263#[derive(Clone, Copy, Debug)]
264pub struct CpiAccount<'a> {
265    address: *const Address,
266    lamports: *const u64,
267    data_len: u64,
268    data: *const u8,
269    owner: *const Address,
270    rent_epoch: u64,
271    is_signer: bool,
272    is_writable: bool,
273    executable: bool,
274    _account_view: PhantomData<&'a AccountView<'a>>,
275}
276
277impl<'a> From<&'a AccountView<'a>> for CpiAccount<'a> {
278    #[inline]
279    fn from(view: &'a AccountView<'a>) -> Self {
280        let raw = view.account_ptr();
281        // SAFETY: account_ptr() returns a valid pointer to the runtime
282        // account struct. The address and owner fields have the same binary
283        // layout as hopper_runtime::Address (#[repr(transparent)] over [u8; 32]).
284        Self {
285            // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
286            address: unsafe { core::ptr::addr_of!((*raw).address) as *const Address },
287            lamports: unsafe { core::ptr::addr_of!((*raw).lamports) },
288            data_len: view.data_len() as u64,
289            data: view.data_ptr_unchecked(),
290            // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
291            owner: unsafe { core::ptr::addr_of!((*raw).owner) as *const Address },
292            rent_epoch: 0,
293            is_signer: view.is_signer(),
294            is_writable: view.is_writable(),
295            executable: view.executable(),
296            _account_view: PhantomData,
297        }
298    }
299}
300
301// ── Seed ─────────────────────────────────────────────────────────────
302
303/// A single PDA seed for CPI signing.
304#[repr(C)]
305#[derive(Debug, Clone)]
306pub struct Seed<'a> {
307    pub(crate) seed: *const u8,
308    pub(crate) len: u64,
309    _bytes: PhantomData<&'a [u8]>,
310}
311
312impl<'a> From<&'a [u8]> for Seed<'a> {
313    #[inline(always)]
314    fn from(bytes: &'a [u8]) -> Self {
315        Self {
316            seed: bytes.as_ptr(),
317            len: bytes.len() as u64,
318            _bytes: PhantomData,
319        }
320    }
321}
322
323impl<'a, const N: usize> From<&'a [u8; N]> for Seed<'a> {
324    #[inline(always)]
325    fn from(bytes: &'a [u8; N]) -> Self {
326        Self {
327            seed: bytes.as_ptr(),
328            len: N as u64,
329            _bytes: PhantomData,
330        }
331    }
332}
333
334impl core::ops::Deref for Seed<'_> {
335    type Target = [u8];
336
337    #[inline(always)]
338    fn deref(&self) -> &[u8] {
339        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
340        unsafe { core::slice::from_raw_parts(self.seed, self.len as usize) }
341    }
342}
343
344// ── Signer ───────────────────────────────────────────────────────────
345
346/// A PDA signer: a set of seeds that derive the signing PDA.
347#[repr(C)]
348#[derive(Debug, Clone)]
349pub struct Signer<'a, 'b> {
350    pub(crate) seeds: *const Seed<'a>,
351    pub(crate) len: u64,
352    _seeds: PhantomData<&'b [Seed<'a>]>,
353}
354
355impl<'a, 'b> From<&'b [Seed<'a>]> for Signer<'a, 'b> {
356    #[inline(always)]
357    fn from(seeds: &'b [Seed<'a>]) -> Self {
358        Self {
359            seeds: seeds.as_ptr(),
360            len: seeds.len() as u64,
361            _seeds: PhantomData,
362        }
363    }
364}
365
366impl<'a, 'b, const N: usize> From<&'b [Seed<'a>; N]> for Signer<'a, 'b> {
367    #[inline(always)]
368    fn from(seeds: &'b [Seed<'a>; N]) -> Self {
369        Self {
370            seeds: seeds.as_ptr(),
371            len: N as u64,
372            _seeds: PhantomData,
373        }
374    }
375}
376
377/// Convenience macro for building an array of `Seed` from expressions.
378///
379/// Usage: `let seeds = seeds!(b"vault", mint_key.as_ref(), &[bump]);`
380#[macro_export]
381macro_rules! seeds {
382    ( $($seed:expr),* $(,)? ) => {
383        [$(
384            $crate::instruction::Seed::from($seed),
385        )*]
386    };
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn stored_account_meta_flags_round_trip() {
395        let key = Address::new_from_array([3; 32]);
396        let meta = StoredAccountMeta::writable_signer(key);
397        assert!(meta.is_signer());
398        assert!(meta.is_writable());
399
400        let ix_meta = meta.to_instruction_account();
401        assert_eq!(ix_meta.address, &key);
402        assert!(ix_meta.is_signer);
403        assert!(ix_meta.is_writable);
404    }
405
406    #[test]
407    fn stored_instruction_builds_instruction_view_without_alloc() {
408        let program = Address::new_from_array([9; 32]);
409        let first = Address::new_from_array([1; 32]);
410        let second = Address::new_from_array([2; 32]);
411        let metas = [
412            StoredAccountMeta::readonly(first),
413            StoredAccountMeta::writable(second),
414        ];
415        let data = [7u8, 8, 9];
416        let stored = StoredInstruction::new(program, &metas, &data).unwrap();
417        let mut out: [MaybeUninit<InstructionAccount<'_>>; 2] =
418            [MaybeUninit::uninit(), MaybeUninit::uninit()];
419
420        let view = stored.to_instruction_view(&mut out).unwrap();
421        assert_eq!(view.program_id, &program);
422        assert_eq!(view.data, &data);
423        assert_eq!(view.accounts.len(), 2);
424        assert_eq!(view.accounts[0].address, &first);
425        assert!(!view.accounts[0].is_writable);
426        assert_eq!(view.accounts[1].address, &second);
427        assert!(view.accounts[1].is_writable);
428    }
429
430    #[test]
431    fn stored_instruction_rejects_small_output_buffer() {
432        let program = Address::new_from_array([9; 32]);
433        let first = Address::new_from_array([1; 32]);
434        let second = Address::new_from_array([2; 32]);
435        let metas = [
436            StoredAccountMeta::readonly(first),
437            StoredAccountMeta::writable(second),
438        ];
439        let stored = StoredInstruction::new(program, &metas, &[]).unwrap();
440        let mut out: [MaybeUninit<InstructionAccount<'_>>; 1] = [MaybeUninit::uninit()];
441
442        assert_eq!(
443            stored.to_instruction_view(&mut out).unwrap_err(),
444            ProgramError::InvalidArgument
445        );
446    }
447}