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 core::marker::PhantomData;
10
11// ── InstructionAccount ───────────────────────────────────────────────
12
13/// Metadata for an account referenced in a CPI instruction.
14#[repr(C)]
15#[derive(Debug, Clone)]
16pub struct InstructionAccount<'a> {
17    /// Public key of the account.
18    pub address: &'a Address,
19    /// Whether the account should be writable.
20    pub is_writable: bool,
21    /// Whether the account should sign.
22    pub is_signer: bool,
23}
24
25impl<'a> InstructionAccount<'a> {
26    /// Construct with explicit flags.
27    #[inline(always)]
28    pub const fn new(address: &'a Address, is_writable: bool, is_signer: bool) -> Self {
29        Self {
30            address,
31            is_writable,
32            is_signer,
33        }
34    }
35
36    /// Read-only, non-signer.
37    #[inline(always)]
38    pub const fn readonly(address: &'a Address) -> Self {
39        Self {
40            address,
41            is_writable: false,
42            is_signer: false,
43        }
44    }
45
46    /// Writable, non-signer.
47    #[inline(always)]
48    pub const fn writable(address: &'a Address) -> Self {
49        Self {
50            address,
51            is_writable: true,
52            is_signer: false,
53        }
54    }
55
56    /// Read-only signer.
57    #[inline(always)]
58    pub const fn readonly_signer(address: &'a Address) -> Self {
59        Self {
60            address,
61            is_writable: false,
62            is_signer: true,
63        }
64    }
65
66    /// Writable signer.
67    #[inline(always)]
68    pub const fn writable_signer(address: &'a Address) -> Self {
69        Self {
70            address,
71            is_writable: true,
72            is_signer: true,
73        }
74    }
75}
76
77impl<'a> From<&'a AccountView> for InstructionAccount<'a> {
78    #[inline(always)]
79    fn from(view: &'a AccountView) -> Self {
80        Self {
81            address: view.address(),
82            is_writable: view.is_writable(),
83            is_signer: view.is_signer(),
84        }
85    }
86}
87
88// ── InstructionView ──────────────────────────────────────────────────
89
90/// A cross-program instruction to invoke.
91#[derive(Debug, Clone)]
92pub struct InstructionView<'a, 'b, 'c, 'd>
93where
94    'a: 'b,
95{
96    /// Program to call.
97    pub program_id: &'c Address,
98    /// Instruction data.
99    pub data: &'d [u8],
100    /// Account metadata.
101    pub accounts: &'b [InstructionAccount<'a>],
102}
103
104// ── CpiAccount (hopper-native backend) ───────────────────────────────
105
106/// C-ABI account info passed to `sol_invoke_signed_c`.
107///
108/// This matches the Solana runtime's expected layout for CPI account infos.
109/// Only available with hopper-native-backend because it requires direct
110/// pointer access into the runtime's account memory.
111#[cfg(feature = "hopper-native-backend")]
112#[repr(C)]
113#[derive(Clone, Copy, Debug)]
114pub struct CpiAccount<'a> {
115    address: *const Address,
116    lamports: *const u64,
117    data_len: u64,
118    data: *const u8,
119    owner: *const Address,
120    rent_epoch: u64,
121    is_signer: bool,
122    is_writable: bool,
123    executable: bool,
124    _account_view: PhantomData<&'a AccountView>,
125}
126
127#[cfg(feature = "hopper-native-backend")]
128impl<'a> From<&'a AccountView> for CpiAccount<'a> {
129    #[inline]
130    fn from(view: &'a AccountView) -> Self {
131        let raw = view.account_ptr();
132        // SAFETY: account_ptr() returns a valid pointer to the runtime
133        // account struct. The address and owner fields have the same binary
134        // layout as hopper_runtime::Address (#[repr(transparent)] over [u8; 32]).
135        Self {
136            // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
137            address: unsafe { core::ptr::addr_of!((*raw).address) as *const Address },
138            lamports: unsafe { core::ptr::addr_of!((*raw).lamports) },
139            data_len: view.data_len() as u64,
140            data: view.data_ptr_unchecked(),
141            // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
142            owner: unsafe { core::ptr::addr_of!((*raw).owner) as *const Address },
143            rent_epoch: 0,
144            is_signer: view.is_signer(),
145            is_writable: view.is_writable(),
146            executable: view.executable(),
147            _account_view: PhantomData,
148        }
149    }
150}
151
152// ── Seed ─────────────────────────────────────────────────────────────
153
154/// A single PDA seed for CPI signing.
155#[repr(C)]
156#[derive(Debug, Clone)]
157pub struct Seed<'a> {
158    pub(crate) seed: *const u8,
159    pub(crate) len: u64,
160    _bytes: PhantomData<&'a [u8]>,
161}
162
163impl<'a> From<&'a [u8]> for Seed<'a> {
164    #[inline(always)]
165    fn from(bytes: &'a [u8]) -> Self {
166        Self {
167            seed: bytes.as_ptr(),
168            len: bytes.len() as u64,
169            _bytes: PhantomData,
170        }
171    }
172}
173
174impl<'a, const N: usize> From<&'a [u8; N]> for Seed<'a> {
175    #[inline(always)]
176    fn from(bytes: &'a [u8; N]) -> Self {
177        Self {
178            seed: bytes.as_ptr(),
179            len: N as u64,
180            _bytes: PhantomData,
181        }
182    }
183}
184
185impl core::ops::Deref for Seed<'_> {
186    type Target = [u8];
187
188    #[inline(always)]
189    fn deref(&self) -> &[u8] {
190        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
191        unsafe { core::slice::from_raw_parts(self.seed, self.len as usize) }
192    }
193}
194
195// ── Signer ───────────────────────────────────────────────────────────
196
197/// A PDA signer: a set of seeds that derive the signing PDA.
198#[repr(C)]
199#[derive(Debug, Clone)]
200pub struct Signer<'a, 'b> {
201    pub(crate) seeds: *const Seed<'a>,
202    pub(crate) len: u64,
203    _seeds: PhantomData<&'b [Seed<'a>]>,
204}
205
206impl<'a, 'b> From<&'b [Seed<'a>]> for Signer<'a, 'b> {
207    #[inline(always)]
208    fn from(seeds: &'b [Seed<'a>]) -> Self {
209        Self {
210            seeds: seeds.as_ptr(),
211            len: seeds.len() as u64,
212            _seeds: PhantomData,
213        }
214    }
215}
216
217impl<'a, 'b, const N: usize> From<&'b [Seed<'a>; N]> for Signer<'a, 'b> {
218    #[inline(always)]
219    fn from(seeds: &'b [Seed<'a>; N]) -> Self {
220        Self {
221            seeds: seeds.as_ptr(),
222            len: N as u64,
223            _seeds: PhantomData,
224        }
225    }
226}
227
228/// Convenience macro for building an array of `Seed` from expressions.
229///
230/// Usage: `let seeds = seeds!(b"vault", mint_key.as_ref(), &[bump]);`
231#[macro_export]
232macro_rules! seeds {
233    ( $($seed:expr),* $(,)? ) => {
234        [$(
235            $crate::instruction::Seed::from($seed),
236        )*]
237    };
238}