Skip to main content

hopper_solana/
interface.rs

1//! Token and Token-2022 interface overlays.
2//!
3//! Hopper keeps the account-owner check separate from the zero-copy
4//! token-account and mint overlays. That gives callers one explicit gate
5//! for `owner in {SPL Token, Token-2022}` and then a shared reader surface
6//! for the base layouts supported by both programs:
7//!
8//! - [`InterfaceTokenAccount`] - token-account-shaped overlay for either
9//!   SPL Token or Token-2022.
10//! - [`InterfaceMint`] - mint-shaped overlay for either program.
11//! - [`TokenProgramKind`] - discriminates which program owns the account.
12//!
13//! The first 165 bytes of an SPL Token Account and the first 165 bytes
14//! of a Token-2022 token account share the same on-disk layout (mint,
15//! owner, amount, delegate, state, …), so the existing zero-copy
16//! readers in [`crate::token`] and [`crate::mint`] work for both. This
17//! module adds the validation gate (owner ∈ {Token, Token-2022}) plus
18//! a polymorphic `transfer_checked` CPI helper that dispatches to the
19//! correct program.
20
21use hopper_runtime::account::AccountView;
22use hopper_runtime::address::Address;
23use hopper_runtime::error::ProgramError;
24use hopper_runtime::instruction::{InstructionAccount, InstructionView, Signer};
25use hopper_runtime::ProgramResult;
26
27use crate::constants::{TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID};
28
29/// Which token program owns this account.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum TokenProgramKind {
32    /// SPL Token (`TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`).
33    Spl,
34    /// SPL Token-2022 (`TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`).
35    Token2022,
36}
37
38impl TokenProgramKind {
39    /// Resolve the program-id address backing this kind.
40    #[inline(always)]
41    pub const fn program_id(self) -> &'static Address {
42        match self {
43            TokenProgramKind::Spl => &TOKEN_PROGRAM_ID,
44            TokenProgramKind::Token2022 => &TOKEN_2022_PROGRAM_ID,
45        }
46    }
47
48    /// Match an account's owner against the two supported programs.
49    ///
50    /// Returns `Err(IncorrectProgramId)` if the account is owned by
51    /// any other program.
52    #[inline(always)]
53    pub fn from_owner(owner: &Address) -> Result<Self, ProgramError> {
54        if owner == &TOKEN_PROGRAM_ID {
55            Ok(TokenProgramKind::Spl)
56        } else if owner == &TOKEN_2022_PROGRAM_ID {
57            Ok(TokenProgramKind::Token2022)
58        } else {
59            Err(ProgramError::IncorrectProgramId)
60        }
61    }
62
63    /// Resolve the kind from an [`AccountView`] using its owner.
64    ///
65    /// Wrapper over [`AccountView::owned_by`] that stays on the safe
66    /// (no-unsafe) side of the runtime API surface.
67    #[inline(always)]
68    pub fn for_account(view: &AccountView) -> Result<Self, ProgramError> {
69        if view.owned_by(&TOKEN_PROGRAM_ID) {
70            Ok(TokenProgramKind::Spl)
71        } else if view.owned_by(&TOKEN_2022_PROGRAM_ID) {
72            Ok(TokenProgramKind::Token2022)
73        } else {
74            Err(ProgramError::IncorrectProgramId)
75        }
76    }
77}
78
79/// Polymorphic SPL Token / Token-2022 token-account overlay.
80///
81/// Construct via [`InterfaceTokenAccount::from_data`] using a borrowed
82/// view of an account body that has already been ownership-checked
83/// via [`TokenProgramKind::for_account`]. The constructor validates
84/// the body is at least [`crate::token::TOKEN_ACCOUNT_LEN`] (165) bytes.
85///
86/// The reader methods delegate to [`crate::token`], which is correct
87/// for both programs because the first 165 bytes of a Token-2022
88/// account match the SPL Token layout exactly.
89///
90/// ```ignore
91/// let kind = TokenProgramKind::for_account(&view)?;
92/// let data = view.try_borrow()?;
93/// let token = InterfaceTokenAccount::from_data(&data, kind)?;
94/// let mint = token.mint()?;
95/// ```
96#[derive(Debug, Clone, Copy)]
97pub struct InterfaceTokenAccount<'a> {
98    /// Raw account body. Always at least 165 bytes.
99    data: &'a [u8],
100    /// Which program owns the account.
101    pub kind: TokenProgramKind,
102}
103
104impl<'a> InterfaceTokenAccount<'a> {
105    /// Wrap a previously-borrowed account body.
106    ///
107    /// Caller is responsible for confirming `kind` matches the
108    /// account's actual owner - usually by calling
109    /// [`TokenProgramKind::for_account`] beforehand.
110    pub fn from_data(data: &'a [u8], kind: TokenProgramKind) -> Result<Self, ProgramError> {
111        if data.len() < crate::token::TOKEN_ACCOUNT_LEN {
112            return Err(ProgramError::InvalidAccountData);
113        }
114        Ok(Self { data, kind })
115    }
116
117    /// The raw account body. Always at least 165 bytes.
118    #[inline(always)]
119    pub fn data(&self) -> &'a [u8] {
120        self.data
121    }
122
123    /// The mint pubkey.
124    #[inline(always)]
125    pub fn mint(&self) -> Result<&'a Address, ProgramError> {
126        crate::token::token_account_mint(self.data)
127    }
128
129    /// The owner pubkey of the token account (the user wallet, not the
130    /// program owning the account).
131    #[inline(always)]
132    pub fn owner(&self) -> Result<&'a Address, ProgramError> {
133        crate::token::token_account_owner(self.data)
134    }
135
136    /// The token amount.
137    #[inline(always)]
138    pub fn amount(&self) -> Result<u64, ProgramError> {
139        crate::token::token_account_amount(self.data)
140    }
141
142    /// The state byte (`0` = uninitialised, `1` = initialised, `2` = frozen).
143    #[inline(always)]
144    pub fn state(&self) -> Result<u8, ProgramError> {
145        crate::token::token_account_state(self.data)
146    }
147
148    /// Convenience: assert the account is initialised.
149    #[inline(always)]
150    pub fn assert_initialized(&self) -> Result<(), ProgramError> {
151        crate::token::check_token_initialized(self.data)
152    }
153
154    /// Convenience: assert the wallet owner matches.
155    #[inline(always)]
156    pub fn assert_owner(&self, expected: &Address) -> Result<(), ProgramError> {
157        crate::token::check_token_owner(self.data, expected)
158    }
159
160    /// Convenience: assert the mint matches.
161    #[inline(always)]
162    pub fn assert_mint(&self, expected: &Address) -> Result<(), ProgramError> {
163        crate::token::check_token_mint(self.data, expected)
164    }
165}
166
167/// Polymorphic SPL Token / Token-2022 mint overlay.
168///
169/// SPL Mint and Token-2022 base mint share the same first 82 bytes
170/// (mint authority COption, supply, decimals, is_init flag, freeze
171/// authority COption). Token-2022 extension bytes begin at offset
172/// 165; this wrapper exposes only the base layout. Use
173/// [`crate::token2022_ext`] helpers for extension parsing.
174#[derive(Debug, Clone, Copy)]
175pub struct InterfaceMint<'a> {
176    data: &'a [u8],
177    /// Which program owns the mint.
178    pub kind: TokenProgramKind,
179}
180
181impl<'a> InterfaceMint<'a> {
182    /// Wrap a previously-borrowed mint body. Caller verifies `kind`
183    /// using [`TokenProgramKind::for_account`].
184    pub fn from_data(data: &'a [u8], kind: TokenProgramKind) -> Result<Self, ProgramError> {
185        if data.len() < crate::mint::MINT_LEN {
186            return Err(ProgramError::InvalidAccountData);
187        }
188        Ok(Self { data, kind })
189    }
190
191    /// The raw mint bytes.
192    #[inline(always)]
193    pub fn data(&self) -> &'a [u8] {
194        self.data
195    }
196
197    /// The mint supply.
198    #[inline(always)]
199    pub fn supply(&self) -> Result<u64, ProgramError> {
200        crate::mint::mint_supply(self.data)
201    }
202
203    /// The mint decimals.
204    #[inline(always)]
205    pub fn decimals(&self) -> Result<u8, ProgramError> {
206        crate::mint::mint_decimals(self.data)
207    }
208
209    /// The mint authority, if set.
210    #[inline(always)]
211    pub fn authority(&self) -> Result<Option<&'a Address>, ProgramError> {
212        crate::mint::mint_authority(self.data)
213    }
214
215    /// The freeze authority, if set.
216    #[inline(always)]
217    pub fn freeze_authority(&self) -> Result<Option<&'a Address>, ProgramError> {
218        crate::mint::mint_freeze_authority(self.data)
219    }
220
221    /// Convenience: assert the mint is initialised.
222    #[inline(always)]
223    pub fn assert_initialized(&self) -> Result<(), ProgramError> {
224        crate::mint::check_mint_initialized(self.data)
225    }
226}
227
228// ── Polymorphic CPI helpers ──────────────────────────────────────────
229
230/// Polymorphic `TransferChecked` CPI that dispatches to the program
231/// that owns the source token account.
232///
233/// The instruction layout is shared between SPL Token and Token-2022:
234/// `[12u8, amount: u64 LE, decimals: u8]` with three accounts (source,
235/// mint, destination, authority). This helper picks the right program
236/// id based on the source account's owner and forwards through the
237/// runtime's checked CPI path.
238#[inline]
239pub fn interface_transfer_checked<'a>(
240    source: &'a AccountView,
241    mint: &'a AccountView,
242    destination: &'a AccountView,
243    authority: &'a AccountView,
244    amount: u64,
245    decimals: u8,
246) -> ProgramResult {
247    interface_transfer_checked_signed(source, mint, destination, authority, amount, decimals, &[])
248}
249
250/// PDA-signing variant of [`interface_transfer_checked`].
251#[inline]
252pub fn interface_transfer_checked_signed<'a>(
253    source: &'a AccountView,
254    mint: &'a AccountView,
255    destination: &'a AccountView,
256    authority: &'a AccountView,
257    amount: u64,
258    decimals: u8,
259    signers: &[Signer],
260) -> ProgramResult {
261    let kind = TokenProgramKind::for_account(source)?;
262
263    let mut data = [0u8; 10];
264    data[0] = 12; // TransferChecked discriminator (shared between Token and Token-2022)
265    data[1..9].copy_from_slice(&amount.to_le_bytes());
266    data[9] = decimals;
267
268    let accounts = [
269        InstructionAccount::writable(source.address()),
270        InstructionAccount::readonly(mint.address()),
271        InstructionAccount::writable(destination.address()),
272        InstructionAccount::readonly_signer(authority.address()),
273    ];
274    let views = [source, mint, destination, authority];
275    let instruction = InstructionView {
276        program_id: kind.program_id(),
277        data: &data,
278        accounts: &accounts,
279    };
280
281    hopper_runtime::cpi::invoke_signed(&instruction, &views, signers)
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn token_program_kind_from_owner_matches_known_programs() {
290        assert_eq!(
291            TokenProgramKind::from_owner(&TOKEN_PROGRAM_ID).unwrap(),
292            TokenProgramKind::Spl,
293        );
294        assert_eq!(
295            TokenProgramKind::from_owner(&TOKEN_2022_PROGRAM_ID).unwrap(),
296            TokenProgramKind::Token2022,
297        );
298    }
299
300    #[test]
301    fn token_program_kind_from_owner_rejects_other_programs() {
302        let other = Address::new_from_array([7u8; 32]);
303        assert!(matches!(
304            TokenProgramKind::from_owner(&other),
305            Err(ProgramError::IncorrectProgramId),
306        ));
307    }
308
309    #[test]
310    fn token_program_kind_program_id_is_stable() {
311        assert_eq!(TokenProgramKind::Spl.program_id(), &TOKEN_PROGRAM_ID,);
312        assert_eq!(
313            TokenProgramKind::Token2022.program_id(),
314            &TOKEN_2022_PROGRAM_ID,
315        );
316    }
317}