Skip to main content

hopper_runtime/
token.rs

1//! Hopper-native SPL Token CPI builders.
2//!
3//! The API is Hopper-owned (builder pattern over `AccountView` / `Signer`).
4//! A builder's CPI checks that no account in the instruction is borrowed and
5//! that no account fills two writable roles, and leaves signer and writable
6//! privileges to the runtime; a multisig authority's signers take the fully
7//! checked bounded path.
8//!
9//! Provides checked-by-default TransferChecked, MintToChecked, BurnChecked,
10//! ApproveChecked, CloseAccount, Revoke, SetAuthority, FreezeAccount,
11//! ThawAccount, SyncNative, and InitializeAccount builders, plus the
12//! administrative set (InitializeMint, InitializeMultisig, InitializeMultisig2,
13//! InitializeImmutableOwner, GetAccountDataSize, WithdrawExcessLamports,
14//! AmountToUiAmount, UiAmountToAmount, UnwrapLamports) and the p-token
15//! `Batch` instruction through [`TokenBatch`].
16//! Multisig owner flows are first-class via bounded signer-account slices.
17//! Deprecated plain Transfer/MintTo/Burn/Approve builders are compiled only
18//! when `legacy-token-instructions` is explicitly enabled.
19//!
20//! ## One encoding, two programs, two sinks
21//!
22//! Every builder encodes its instruction bytes and account metas exactly
23//! once, in its [`TokenInstruction::emit`] impl. `invoke()` and the
24//! `invoke_signed` / `invoke_multisig` family send that encoding to SPL
25//! Token; [`invoke_on`](TransferChecked::invoke_on) sends it to an explicit
26//! [`TokenProgram`], and [`invoke_for_owner`](TransferChecked::invoke_for_owner)
27//! to whichever of the two programs owns the builder's first account (one
28//! 32-byte compare, and a refusal for any other owner). The same `emit` can
29//! also append the instruction to a [`TokenBatch`], which sends several
30//! token instructions in one CPI.
31
32use crate::account::AccountView;
33use crate::address::Address;
34use crate::borrow::Ref;
35use crate::error::ProgramError;
36use crate::foreign::{ExplainExternal, ExternalAccount, ExternalExplainSink, ExternalZeroCopy};
37use crate::instruction::{InstructionAccount, InstructionView, Signer};
38use crate::ProgramResult;
39use core::mem::MaybeUninit;
40
41pub use crate::token_admin::{
42    return_data_string, return_data_u64, AmountToUiAmount, GetAccountDataSize,
43    InitializeImmutableOwner, InitializeMint, InitializeMultisig, InitializeMultisig2,
44    UiAmountToAmount, UnwrapLamports, WithdrawExcessLamports, MAX_UI_AMOUNT_LEN,
45};
46pub use crate::token_batch::TokenBatch;
47pub use crate::token_mint::{InitializeMint2, MintConfig, MintPlan, MintProgram};
48
49/// Token-2022 program address.
50pub const TOKEN_2022_PROGRAM_ID: Address = Address::new_from_array(crate::__decode_base58_32(
51    "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
52));
53
54/// The two token programs every builder in this module can target.
55///
56/// The wire format of the shared instruction set is identical on both, so a
57/// builder's encoding does not change; only the program id in the CPI does.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum TokenProgram {
60    /// SPL Token, `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`.
61    Legacy,
62    /// Token-2022, `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`.
63    Token2022,
64}
65
66impl TokenProgram {
67    /// The program id.
68    #[inline(always)]
69    pub const fn address(self) -> &'static Address {
70        match self {
71            Self::Legacy => &TOKEN_PROGRAM_ID,
72            Self::Token2022 => &TOKEN_2022_PROGRAM_ID,
73        }
74    }
75
76    /// The program behind an address, if it is one of the two.
77    #[inline]
78    pub fn from_address(address: &Address) -> Option<Self> {
79        if address == &TOKEN_PROGRAM_ID {
80            Some(Self::Legacy)
81        } else if address == &TOKEN_2022_PROGRAM_ID {
82            Some(Self::Token2022)
83        } else {
84            None
85        }
86    }
87
88    /// The program that owns `account`: a token account, a mint, or a
89    /// multisig. Anything owned by another program is refused with
90    /// `IncorrectProgramId`, so a builder driven by this never sends a
91    /// token instruction to a program that is not a token program.
92    #[inline]
93    pub fn owning(account: &AccountView<'_>) -> Result<Self, ProgramError> {
94        if account.owned_by(&TOKEN_PROGRAM_ID) {
95            Ok(Self::Legacy)
96        } else if account.owned_by(&TOKEN_2022_PROGRAM_ID) {
97            Ok(Self::Token2022)
98        } else {
99            Err(ProgramError::IncorrectProgramId)
100        }
101    }
102
103    /// The program an executable account in the instruction stands for.
104    /// Refuses any address that is not one of the two token programs.
105    #[inline]
106    pub fn from_program_account(program: &AccountView<'_>) -> Result<Self, ProgramError> {
107        Self::from_address(program.address()).ok_or(ProgramError::IncorrectProgramId)
108    }
109}
110
111/// A run of accounts appended after a builder's fixed accounts, all with
112/// the same privileges: the multisig signers of an authority, the member
113/// list of a new multisig, or the source accounts of a withheld-fee sweep.
114#[derive(Clone, Copy)]
115pub struct Trailing<'s, 'a> {
116    pub views: &'s [&'a AccountView<'a>],
117    pub writable: bool,
118    pub signer: bool,
119}
120
121impl<'s, 'a> Trailing<'s, 'a> {
122    /// Read-only signers (a multisig authority's signer set).
123    #[inline(always)]
124    pub const fn signers(views: &'s [&'a AccountView<'a>]) -> Self {
125        Self {
126            views,
127            writable: false,
128            signer: true,
129        }
130    }
131
132    /// Read-only non-signers (the member list of `InitializeMultisig`).
133    #[inline(always)]
134    pub const fn readonly(views: &'s [&'a AccountView<'a>]) -> Self {
135        Self {
136            views,
137            writable: false,
138            signer: false,
139        }
140    }
141
142    /// Writable non-signers (the sources of a withheld-fee harvest).
143    #[inline(always)]
144    pub const fn writable(views: &'s [&'a AccountView<'a>]) -> Self {
145        Self {
146            views,
147            writable: true,
148            signer: false,
149        }
150    }
151}
152
153/// Where a builder's encoded instruction goes.
154///
155/// Two sinks exist: the CPI itself (what every `invoke*` method uses) and
156/// [`TokenBatch`], which collects several instructions for one `Batch` CPI.
157/// A builder never encodes differently for the two.
158pub trait TokenSink<'a> {
159    /// Receive one encoded instruction: its data, its fixed account metas
160    /// and views in order, and zero or more trailing runs appended after
161    /// them.
162    fn emit<const N: usize>(
163        &mut self,
164        data: &[u8],
165        accounts: [InstructionAccount<'a>; N],
166        views: [&'a AccountView<'a>; N],
167        trailing: &[Trailing<'_, 'a>],
168    ) -> ProgramResult;
169}
170
171/// An SPL Token / Token-2022 instruction builder: something that can encode
172/// itself into a [`TokenSink`].
173///
174/// `multisig_signers` are the signer accounts of a multisig authority
175/// (at most [`MAX_TOKEN_MULTISIG_SIGNERS`]); builders without an authority
176/// ignore the slice.
177pub trait TokenInstruction<'a> {
178    fn emit(
179        &self,
180        multisig_signers: &[&'a AccountView<'a>],
181        sink: &mut impl TokenSink<'a>,
182    ) -> ProgramResult;
183}
184
185/// The CPI sink: sends the encoded instruction to `program`, signed by
186/// `signers`.
187pub(crate) struct Invoke<'p, 's, 'x, 'y> {
188    pub(crate) program: &'p Address,
189    pub(crate) signers: &'s [Signer<'x, 'y>],
190}
191
192impl<'s, 'x, 'y> Invoke<'static, 's, 'x, 'y> {
193    /// The SPL Token program, the historical default of every `invoke*`.
194    #[inline(always)]
195    pub(crate) const fn legacy(signers: &'s [Signer<'x, 'y>]) -> Self {
196        Self {
197            program: &TOKEN_PROGRAM_ID,
198            signers,
199        }
200    }
201
202    /// The Token-2022 program.
203    #[inline(always)]
204    pub(crate) const fn token_2022(signers: &'s [Signer<'x, 'y>]) -> Self {
205        Self {
206            program: &TOKEN_2022_PROGRAM_ID,
207            signers,
208        }
209    }
210}
211
212impl<'a> TokenSink<'a> for Invoke<'_, '_, '_, '_> {
213    #[inline(always)]
214    fn emit<const N: usize>(
215        &mut self,
216        data: &[u8],
217        accounts: [InstructionAccount<'a>; N],
218        views: [&'a AccountView<'a>; N],
219        trailing: &[Trailing<'_, 'a>],
220    ) -> ProgramResult {
221        // Without multisig signers every meta is built from the view beside
222        // it, so the builder tier applies: borrow checks, the lamport gate,
223        // and the repeated-writable refusal, with privileges left to the
224        // runtime. A multisig authority's trailing signers take the bounded
225        // path, which checks each of them.
226        if trailing.iter().all(|run| run.views.is_empty()) {
227            let instruction = InstructionView {
228                program_id: self.program,
229                data,
230                accounts: &accounts,
231            };
232            return crate::cpi::invoke_signed_builder_distinct(&instruction, &views, self.signers);
233        }
234        invoke_token_signed(self.program, data, accounts, views, trailing, self.signers)
235    }
236}
237
238/// The program-selecting entry points every builder gets: `invoke_on` for
239/// an explicit [`TokenProgram`] and `invoke_for_owner` for the program that
240/// owns the builder's first account. With no PDA `signers`, the same direct
241/// signer checks as `invoke()` / `invoke_multisig()` run first.
242macro_rules! token_program_methods {
243    ($name:ident, owner = $owner:ident $(, authority = $auth:ident)?) => {
244        impl $name<'_> {
245            /// Send this instruction to an explicit token program.
246            ///
247            /// `multisig_signers` is empty for a single-key authority; when
248            /// `signers` is empty the authority (or every multisig signer)
249            /// must have signed the transaction directly.
250            #[inline]
251            pub fn invoke_on(
252                &self,
253                program: TokenProgram,
254                multisig_signers: &[&AccountView<'_>],
255                signers: &[Signer<'_, '_>],
256            ) -> ProgramResult {
257                $(
258                    if signers.is_empty() {
259                        if multisig_signers.is_empty() {
260                            require_authority_signed_direct(self.$auth)?;
261                        } else {
262                            require_multisig_signers_direct(multisig_signers)?;
263                        }
264                    }
265                )?
266                self.emit(
267                    multisig_signers,
268                    &mut Invoke {
269                        program: program.address(),
270                        signers,
271                    },
272                )
273            }
274
275            /// Send this instruction to whichever token program owns
276            #[doc = concat!("`", stringify!($owner), "`")]
277            /// (one 32-byte compare). Any other owner is refused with
278            /// `IncorrectProgramId` before the CPI.
279            #[inline]
280            pub fn invoke_for_owner(
281                &self,
282                multisig_signers: &[&AccountView<'_>],
283                signers: &[Signer<'_, '_>],
284            ) -> ProgramResult {
285                self.invoke_on(TokenProgram::owning(self.$owner)?, multisig_signers, signers)
286            }
287        }
288    };
289}
290pub(crate) use token_program_methods;
291
292/// SPL Token multisig accounts support at most 11 signer accounts.
293pub const MAX_TOKEN_MULTISIG_SIGNERS: usize = 11;
294
295/// Fail-fast authority-signer precondition for the `invoke()` path.
296///
297/// The SPL token program enforces the signer requirement itself,
298/// but the resulting error is a raw CPI failure without context.
299/// This helper surfaces a Hopper-branded
300/// `ProgramError::MissingRequiredSignature` before the CPI runs so
301/// the caller sees exactly which field is wrong. Safety is enforced at
302/// the API boundary, not left to convention.
303///
304/// Intentionally only applied on `invoke()`. The `invoke_signed()`
305/// path is the explicit "I am signing programmatically with these
306/// PDA seeds" contract. recomputing PDAs here would duplicate work
307/// the SPL token program is about to do anyway. In the PDA path
308/// the CPI itself is the authoritative check.
309#[inline(always)]
310pub(crate) fn require_authority_signed_direct(authority: &AccountView<'_>) -> ProgramResult {
311    if authority.is_signer() {
312        Ok(())
313    } else {
314        Err(ProgramError::MissingRequiredSignature)
315    }
316}
317
318#[inline(always)]
319pub(crate) fn authority_meta<'a>(
320    authority: &'a AccountView<'a>,
321    multisig_signers: &[&'a AccountView<'a>],
322) -> InstructionAccount<'a> {
323    if multisig_signers.is_empty() {
324        InstructionAccount::readonly_signer(authority.address())
325    } else {
326        InstructionAccount::readonly(authority.address())
327    }
328}
329
330#[inline]
331pub(crate) fn require_multisig_signers_direct(
332    multisig_signers: &[&AccountView<'_>],
333) -> ProgramResult {
334    if multisig_signers.len() > MAX_TOKEN_MULTISIG_SIGNERS {
335        return Err(ProgramError::InvalidArgument);
336    }
337    for signer in multisig_signers {
338        require_authority_signed_direct(signer)?;
339    }
340    Ok(())
341}
342
343/// Byte-exact instruction-data encoders for the SPL Token CPI wire format.
344///
345/// # Why this module is `pub`
346///
347/// Every SPL Token builder in this file constructs its instruction-data
348/// buffer by calling exactly one of these functions before handing the bytes
349/// to [`crate::cpi`]. They are the single, **shipped** source of truth for the
350/// SPL Token wire format, the exact bytes that leave the program on a CPI,
351/// not a mirror or a parallel re-implementation.
352///
353/// They are exposed as `#[doc(hidden)] pub` for one reason: so the Kani layout
354/// proofs in the `hopper-token` crate can call the shipped encoders directly
355/// and prove, over fully symbolic inputs, that the bytes the CPI path emits
356/// carry the canonical discriminator, field order/offsets, endianness, and
357/// total length. Proving the shipped functions (rather than a copy of them) is
358/// what lets Hopper claim the encoders themselves are formally verified.
359///
360/// This is deliberately **not** a stability surface: the module is
361/// `#[doc(hidden)]` and may change at any time. Depend on the builder structs
362/// ([`TransferChecked`], [`MintToChecked`], …), never on `encoders`.
363///
364/// Each function is `#[inline(always)]`, so delegating to it from a builder is
365/// zero-cost: the emitted bytes and codegen are identical to the previous
366/// inline construction.
367#[doc(hidden)]
368pub mod encoders {
369    /// `[disc][amount: u64 LE]`, the 9-byte shape shared by the plain
370    /// `Transfer` (3), `Approve` (4), `MintTo` (7), and `Burn` (8)
371    /// instructions.
372    #[inline(always)]
373    fn amount_ix(disc: u8, amount: u64) -> [u8; 9] {
374        let mut data = [0u8; 9];
375        data[0] = disc;
376        data[1..9].copy_from_slice(&amount.to_le_bytes());
377        data
378    }
379
380    /// `[disc][amount: u64 LE][decimals: u8]`, the 10-byte shape shared by
381    /// the `TransferChecked` (12), `ApproveChecked` (13), `MintToChecked`
382    /// (14), and `BurnChecked` (15) instructions.
383    #[inline(always)]
384    fn amount_checked_ix(disc: u8, amount: u64, decimals: u8) -> [u8; 10] {
385        let mut data = [0u8; 10];
386        data[0] = disc;
387        data[1..9].copy_from_slice(&amount.to_le_bytes());
388        data[9] = decimals;
389        data
390    }
391
392    /// SPL Token `Transfer { amount }`, `[3][amount: u64 LE]` (9 bytes).
393    #[inline(always)]
394    pub fn encode_transfer(amount: u64) -> [u8; 9] {
395        amount_ix(3, amount)
396    }
397
398    /// SPL Token `Approve { amount }`, `[4][amount: u64 LE]` (9 bytes).
399    #[inline(always)]
400    pub fn encode_approve(amount: u64) -> [u8; 9] {
401        amount_ix(4, amount)
402    }
403
404    /// SPL Token `MintTo { amount }`, `[7][amount: u64 LE]` (9 bytes).
405    #[inline(always)]
406    pub fn encode_mint_to(amount: u64) -> [u8; 9] {
407        amount_ix(7, amount)
408    }
409
410    /// SPL Token `Burn { amount }`, `[8][amount: u64 LE]` (9 bytes).
411    #[inline(always)]
412    pub fn encode_burn(amount: u64) -> [u8; 9] {
413        amount_ix(8, amount)
414    }
415
416    /// SPL Token `TransferChecked { amount, decimals }`,
417    /// `[12][amount: u64 LE][decimals: u8]` (10 bytes).
418    #[inline(always)]
419    pub fn encode_transfer_checked(amount: u64, decimals: u8) -> [u8; 10] {
420        amount_checked_ix(12, amount, decimals)
421    }
422
423    /// SPL Token `ApproveChecked { amount, decimals }`,
424    /// `[13][amount: u64 LE][decimals: u8]` (10 bytes).
425    #[inline(always)]
426    pub fn encode_approve_checked(amount: u64, decimals: u8) -> [u8; 10] {
427        amount_checked_ix(13, amount, decimals)
428    }
429
430    /// SPL Token `MintToChecked { amount, decimals }`,
431    /// `[14][amount: u64 LE][decimals: u8]` (10 bytes).
432    #[inline(always)]
433    pub fn encode_mint_to_checked(amount: u64, decimals: u8) -> [u8; 10] {
434        amount_checked_ix(14, amount, decimals)
435    }
436
437    /// SPL Token `BurnChecked { amount, decimals }`,
438    /// `[15][amount: u64 LE][decimals: u8]` (10 bytes).
439    #[inline(always)]
440    pub fn encode_burn_checked(amount: u64, decimals: u8) -> [u8; 10] {
441        amount_checked_ix(15, amount, decimals)
442    }
443
444    /// SPL Token `Revoke`, `[5]` (1 byte).
445    #[inline(always)]
446    pub fn encode_revoke() -> [u8; 1] {
447        [5]
448    }
449
450    /// SPL Token `CloseAccount`, `[9]` (1 byte).
451    #[inline(always)]
452    pub fn encode_close_account() -> [u8; 1] {
453        [9]
454    }
455
456    /// SPL Token `FreezeAccount`, `[10]` (1 byte).
457    #[inline(always)]
458    pub fn encode_freeze_account() -> [u8; 1] {
459        [10]
460    }
461
462    /// SPL Token `ThawAccount`, `[11]` (1 byte).
463    #[inline(always)]
464    pub fn encode_thaw_account() -> [u8; 1] {
465        [11]
466    }
467
468    /// SPL Token `SyncNative`, `[17]` (1 byte).
469    #[inline(always)]
470    pub fn encode_sync_native() -> [u8; 1] {
471        [17]
472    }
473
474    /// SPL Token `InitializeAccount`, `[1]` (1 byte). Mint/owner/rent travel
475    /// in the account-meta list, not the instruction data.
476    #[inline(always)]
477    pub fn encode_initialize_account() -> [u8; 1] {
478        [1]
479    }
480
481    /// SPL Token `InitializeAccount2`/`InitializeAccount3 { owner }`,
482    /// `[disc][owner: 32 bytes]` (33 bytes). `disc` is 16 for
483    /// `InitializeAccount2` and 18 for `InitializeAccount3`.
484    #[inline(always)]
485    pub fn encode_initialize_account_with_owner(discriminator: u8, owner: &[u8; 32]) -> [u8; 33] {
486        let mut data = [0u8; 33];
487        data[0] = discriminator;
488        data[1..33].copy_from_slice(owner);
489        data
490    }
491
492    /// SPL Token `SetAuthority { authority_type, new_authority }`.
493    ///
494    /// Layout: `[6][authority_type: u8][COption tag: u8]`, followed by
495    /// `[new_authority: 32 bytes]` when `new_authority` is `Some`. The tag
496    /// byte is 1 (`Some`) or 0 (`None`). Returns the fixed 35-byte buffer and
497    /// the number of meaningful bytes: 35 for `Some`, 3 for `None`.
498    #[inline(always)]
499    pub fn encode_set_authority(
500        authority_type: u8,
501        new_authority: Option<&[u8; 32]>,
502    ) -> ([u8; 35], usize) {
503        let mut data = [0u8; 35];
504        data[0] = 6;
505        data[1] = authority_type;
506        match new_authority {
507            Some(key) => {
508                data[2] = 1;
509                data[3..35].copy_from_slice(key);
510                (data, 35)
511            }
512            None => {
513                data[2] = 0;
514                (data, 3)
515            }
516        }
517    }
518
519    /// SPL Token `InitializeMint { decimals, mint_authority, freeze_authority }`,
520    /// `[0][decimals][mint_authority: 32][COption<freeze_authority>]`: 35 bytes
521    /// without a freeze authority, 67 with one.
522    #[inline(always)]
523    pub fn encode_initialize_mint(
524        decimals: u8,
525        mint_authority: &[u8; 32],
526        freeze_authority: Option<&[u8; 32]>,
527    ) -> ([u8; 67], usize) {
528        let mut data = [0u8; 67];
529        data[0] = 0;
530        data[1] = decimals;
531        data[2..34].copy_from_slice(mint_authority);
532        match freeze_authority {
533            Some(key) => {
534                data[34] = 1;
535                data[35..67].copy_from_slice(key);
536                (data, 67)
537            }
538            None => (data, 35),
539        }
540    }
541
542    /// SPL Token `InitializeMultisig { m }`, `[2][m]`.
543    #[inline(always)]
544    pub fn encode_initialize_multisig(m: u8) -> [u8; 2] {
545        [2, m]
546    }
547
548    /// SPL Token `InitializeMultisig2 { m }`, `[19][m]`.
549    #[inline(always)]
550    pub fn encode_initialize_multisig2(m: u8) -> [u8; 2] {
551        [19, m]
552    }
553
554    /// SPL Token `InitializeImmutableOwner`, `[22]`.
555    #[inline(always)]
556    pub fn encode_initialize_immutable_owner() -> [u8; 1] {
557        [22]
558    }
559
560    /// The most extension types one `GetAccountDataSize` or `Reallocate`
561    /// carries. Token-2022 defines 29; the buffer leaves room for growth.
562    pub const MAX_EXTENSION_TYPES: usize = 32;
563
564    /// `[disc][extension_type: u16 LE]*`, the shape of Token-2022
565    /// `GetAccountDataSize` (21) and `Reallocate` (29); SPL Token accepts the
566    /// bare discriminator. `None` when more than [`MAX_EXTENSION_TYPES`]
567    /// types are given.
568    #[inline(always)]
569    pub fn encode_extension_types(
570        disc: u8,
571        extension_types: &[u16],
572    ) -> Option<([u8; 1 + 2 * MAX_EXTENSION_TYPES], usize)> {
573        if extension_types.len() > MAX_EXTENSION_TYPES {
574            return None;
575        }
576        let mut data = [0u8; 1 + 2 * MAX_EXTENSION_TYPES];
577        data[0] = disc;
578        let mut at = 1;
579        for ext in extension_types {
580            data[at..at + 2].copy_from_slice(&ext.to_le_bytes());
581            at += 2;
582        }
583        Some((data, at))
584    }
585
586    /// SPL Token `WithdrawExcessLamports`, `[38]`.
587    #[inline(always)]
588    pub fn encode_withdraw_excess_lamports() -> [u8; 1] {
589        [38]
590    }
591
592    /// SPL Token `AmountToUiAmount { amount }`, `[23][amount: u64 LE]`.
593    #[inline(always)]
594    pub fn encode_amount_to_ui_amount(amount: u64) -> [u8; 9] {
595        amount_ix(23, amount)
596    }
597
598    /// The longest UI-amount string `UiAmountToAmount` carries: the
599    /// instruction is one byte of discriminator plus at most 254 bytes of
600    /// UTF-8.
601    pub const MAX_UI_AMOUNT_LEN: usize = 254;
602
603    /// SPL Token `UiAmountToAmount { ui_amount }`, `[24][utf-8 bytes]`.
604    /// `None` when the string is longer than [`MAX_UI_AMOUNT_LEN`].
605    #[inline(always)]
606    pub fn encode_ui_amount_to_amount(
607        ui_amount: &str,
608    ) -> Option<([u8; 1 + MAX_UI_AMOUNT_LEN], usize)> {
609        let bytes = ui_amount.as_bytes();
610        if bytes.len() > MAX_UI_AMOUNT_LEN {
611            return None;
612        }
613        let mut data = [0u8; 1 + MAX_UI_AMOUNT_LEN];
614        data[0] = 24;
615        data[1..1 + bytes.len()].copy_from_slice(bytes);
616        Some((data, 1 + bytes.len()))
617    }
618
619    /// p-token `UnwrapLamports { amount }`, `[45][0]` for the whole balance
620    /// or `[45][1][amount: u64 LE]` for a part of it.
621    #[inline(always)]
622    pub fn encode_unwrap_lamports(amount: Option<u64>) -> ([u8; 10], usize) {
623        let mut data = [0u8; 10];
624        data[0] = 45;
625        match amount {
626            Some(amount) => {
627                data[1] = 1;
628                data[2..10].copy_from_slice(&amount.to_le_bytes());
629                (data, 10)
630            }
631            None => (data, 2),
632        }
633    }
634}
635
636#[inline]
637pub(crate) fn invoke_token_signed<'a, const FIXED: usize>(
638    program: &Address,
639    data: &[u8],
640    fixed_accounts: [InstructionAccount<'a>; FIXED],
641    fixed_views: [&'a AccountView<'a>; FIXED],
642    trailing: &[Trailing<'_, 'a>],
643    signer_seeds: &[Signer<'_, '_>],
644) -> ProgramResult {
645    let mut total = FIXED;
646    for run in trailing {
647        if run.signer && run.views.len() > MAX_TOKEN_MULTISIG_SIGNERS {
648            return Err(ProgramError::InvalidArgument);
649        }
650        total = total
651            .checked_add(run.views.len())
652            .ok_or(ProgramError::ArithmeticOverflow)?;
653    }
654    if total > crate::cpi::MAX_STATIC_CPI_ACCOUNTS {
655        return Err(ProgramError::InvalidArgument);
656    }
657
658    let mut accounts: [MaybeUninit<InstructionAccount<'a>>; crate::cpi::MAX_STATIC_CPI_ACCOUNTS] =
659        [MaybeUninit::uninit(); crate::cpi::MAX_STATIC_CPI_ACCOUNTS];
660    let mut views: [MaybeUninit<&'a AccountView<'a>>; crate::cpi::MAX_STATIC_CPI_ACCOUNTS] =
661        [MaybeUninit::uninit(); crate::cpi::MAX_STATIC_CPI_ACCOUNTS];
662
663    let mut index = 0;
664    while index < FIXED {
665        accounts[index].write(fixed_accounts[index]);
666        views[index].write(fixed_views[index]);
667        index += 1;
668    }
669    for run in trailing {
670        for view in run.views {
671            accounts[index].write(InstructionAccount::new(
672                view.address(),
673                run.writable,
674                run.signer,
675            ));
676            views[index].write(*view);
677            index += 1;
678        }
679    }
680
681    // SAFETY: slots in 0..total were initialized above, and `total` never
682    // exceeds the fixed buffer capacity checked before writes.
683    let accounts = unsafe {
684        core::slice::from_raw_parts(accounts.as_ptr() as *const InstructionAccount<'a>, total)
685    };
686    // SAFETY: mirrors `accounts`; every view slot in 0..total was initialized.
687    let views =
688        unsafe { core::slice::from_raw_parts(views.as_ptr() as *const &'a AccountView<'a>, total) };
689
690    let instruction = InstructionView {
691        program_id: program,
692        data,
693        accounts,
694    };
695    crate::cpi::invoke_signed_with_bounds::<{ crate::cpi::MAX_STATIC_CPI_ACCOUNTS }>(
696        &instruction,
697        views,
698        signer_seeds,
699    )
700}
701
702/// Verify an SPL Token account's `owner` field matches `authority.key()`.
703///
704/// SPL TokenAccount layout: bytes `[32..64]` are the `owner` pubkey
705/// (the authority allowed to move tokens out of this account). The
706/// SPL Token program checks this on every transfer/approve/burn, but
707/// Hopper's pre-check surfaces a Hopper-branded error before the CPI
708/// so a misconfigured invocation fails with `IncorrectAuthority`
709/// instead of an opaque CPI failure.
710///
711/// This is the load-bearing helper behind the
712/// `#[hopper::program(enforce_token_checks = true)]` contract: the
713/// macro emits `HOPPER_PROGRAM_POLICY.enforce_token_checks = true`,
714/// and handlers opt into the strict invoke paths
715/// ([`TransferChecked::invoke_strict`] etc.) to get this check
716/// auto-injected. Handlers can also call it directly when they reach
717/// outside the typed-context envelope.
718///
719/// Returns `Err(ProgramError::AccountDataTooSmall)` if the token
720/// account's data buffer is too short (not a valid SPL TokenAccount).
721#[inline]
722pub fn require_token_authority(
723    token_account: &AccountView<'_>,
724    authority: &AccountView<'_>,
725) -> ProgramResult {
726    // SPL TokenAccount.owner lives at bytes 32..64. The buffer must
727    // be at least 64 bytes; a valid TokenAccount is exactly 165 on
728    // legacy Token, variable on Token-2022 but always >= 165.
729    let data = token_account
730        .try_borrow()
731        .map_err(|_| ProgramError::AccountBorrowFailed)?;
732    if data.len() < 64 {
733        return Err(ProgramError::AccountDataTooSmall);
734    }
735    // Word-compare the owner field in place: no 32-byte copy.
736    if crate::address::keys_eq_bytes(&data[32..64], authority.address().as_array()) {
737        Ok(())
738    } else {
739        Err(ProgramError::IncorrectAuthority)
740    }
741}
742
743/// Verify an SPL Token account's `owner` field matches a pubkey
744/// supplied directly (i.e. not wrapped in an `AccountView`).
745///
746/// This is the sibling of [`require_token_authority`], differing only
747/// in its argument shape: it takes `&Address` rather than
748/// `&AccountView<'_>` for the expected authority. The declarative
749/// `#[account(token::authority = X)]` attribute lowers to this form
750/// because the user's expression might resolve to a constant address,
751/// a cached field, or another account's key. all of which are
752/// `&Address` by the time the check runs, none of them necessarily
753/// wrapped in an `AccountView`.
754#[inline]
755pub fn require_token_owner_eq(
756    token_account: &AccountView<'_>,
757    expected_owner: &Address,
758) -> ProgramResult {
759    let data = token_account
760        .try_borrow()
761        .map_err(|_| ProgramError::AccountBorrowFailed)?;
762    if data.len() < 64 {
763        return Err(ProgramError::AccountDataTooSmall);
764    }
765    // Word-compare the owner field in place: no 32-byte copy.
766    if crate::address::keys_eq_bytes(&data[32..64], expected_owner.as_array()) {
767        Ok(())
768    } else {
769        Err(ProgramError::IncorrectAuthority)
770    }
771}
772
773/// Verify an SPL Token account's `mint` field matches `expected_mint`.
774///
775/// SPL TokenAccount layout: bytes `[0..32]` are the `mint` pubkey.
776/// Token-2022 extensions never shift the base-layout prefix. the
777/// TLV extensions live past byte 165 behind the account-type
778/// discriminator, so reading bytes 0..32 is valid for both Token
779/// and Token-2022 accounts.
780///
781/// This is the precondition behind Hopper's `#[account(token::mint = X)]`
782/// attribute. It surfaces a Hopper-branded `InvalidAccountData` error
783/// before any downstream CPI runs, so a user-visible failure clearly
784/// points at "wrong mint" rather than an opaque SPL token error.
785///
786/// ## Design notes
787///
788/// The check reads the exact 32 bytes of interest directly from the
789/// already-borrowed data buffer: no extra crate dependencies, no full-struct
790/// deserialize, and the check is trivially inlinable.
791#[inline]
792pub fn require_token_mint(
793    token_account: &AccountView<'_>,
794    expected_mint: &Address,
795) -> ProgramResult {
796    let data = token_account
797        .try_borrow()
798        .map_err(|_| ProgramError::AccountBorrowFailed)?;
799    if data.len() < 32 {
800        return Err(ProgramError::AccountDataTooSmall);
801    }
802    // Word-compare the mint field in place: no 32-byte copy.
803    if crate::address::keys_eq_bytes(&data[0..32], expected_mint.as_array()) {
804        Ok(())
805    } else {
806        Err(ProgramError::InvalidAccountData)
807    }
808}
809
810/// Verify an SPL Mint account's `mint_authority` COption field
811/// matches `expected_authority`.
812///
813/// SPL Mint layout (82 bytes total):
814/// - `0..4`: COption tag for mint_authority (u32 LE; 0 = None, 1 = Some)
815/// - `4..36`: mint_authority pubkey (only meaningful when tag == 1)
816/// - `36..44`: supply (u64 LE)
817/// - `44`: decimals
818/// - `45`: is_initialized
819/// - `46..50`: COption tag for freeze_authority
820/// - `50..82`: freeze_authority pubkey
821///
822/// Behavior: if the tag says `None`, the check fails with
823/// `InvalidAccountData` (the caller asked for a specific authority
824/// but the mint has none). If the tag says `Some` and the stored
825/// pubkey does not match, the check fails with `IncorrectAuthority`.
826/// Separating the two error codes lets callers tell "no authority at
827/// all" apart from "wrong authority".
828#[inline]
829pub fn require_mint_authority(
830    mint_account: &AccountView<'_>,
831    expected_authority: &Address,
832) -> ProgramResult {
833    let data = mint_account
834        .try_borrow()
835        .map_err(|_| ProgramError::AccountBorrowFailed)?;
836    if data.len() < 46 {
837        return Err(ProgramError::AccountDataTooSmall);
838    }
839    let tag = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
840    if tag != 1 {
841        // Tag value 0 = None; any other non-one value is malformed.
842        return Err(ProgramError::InvalidAccountData);
843    }
844    // Word-compare the authority field in place: no 32-byte copy.
845    if crate::address::keys_eq_bytes(&data[4..36], expected_authority.as_array()) {
846        Ok(())
847    } else {
848        Err(ProgramError::IncorrectAuthority)
849    }
850}
851
852/// Verify an SPL Mint account's `decimals` byte matches `expected`.
853///
854/// Reads byte 44 of the Mint layout. Pairs with `require_mint_authority`
855/// to express the full `#[account(mint::authority = X, mint::decimals = N)]`
856/// Anchor-compat syntax with zero additional crate dependencies.
857#[inline]
858pub fn require_mint_decimals(mint_account: &AccountView<'_>, expected: u8) -> ProgramResult {
859    let data = mint_account
860        .try_borrow()
861        .map_err(|_| ProgramError::AccountBorrowFailed)?;
862    if data.len() < 45 {
863        return Err(ProgramError::AccountDataTooSmall);
864    }
865    if data[44] == expected {
866        Ok(())
867    } else {
868        Err(ProgramError::InvalidAccountData)
869    }
870}
871
872/// Verify an SPL Mint account's `freeze_authority` COption field
873/// matches `expected_freeze`.
874///
875/// Same shape as [`require_mint_authority`] but reads the second
876/// COption (bytes 46..50 for tag, 50..82 for pubkey). Exposed so the
877/// macro surface can support a future `mint::freeze_authority = X`
878/// constraint without another runtime change.
879#[inline]
880pub fn require_mint_freeze_authority(
881    mint_account: &AccountView<'_>,
882    expected_freeze: &Address,
883) -> ProgramResult {
884    let data = mint_account
885        .try_borrow()
886        .map_err(|_| ProgramError::AccountBorrowFailed)?;
887    if data.len() < 82 {
888        return Err(ProgramError::AccountDataTooSmall);
889    }
890    let tag = u32::from_le_bytes([data[46], data[47], data[48], data[49]]);
891    if tag != 1 {
892        return Err(ProgramError::InvalidAccountData);
893    }
894    // Word-compare the freeze-authority field in place: no 32-byte copy.
895    if crate::address::keys_eq_bytes(&data[50..82], expected_freeze.as_array()) {
896        Ok(())
897    } else {
898        Err(ProgramError::IncorrectAuthority)
899    }
900}
901
902// ---------------------------------------------------------------------
903
904/// Builder for SPL Token Transfer (instruction index 3).
905///
906/// # Prefer [`TransferChecked`]
907///
908/// The plain instruction carries neither an explicit mint account nor decimals.
909/// The token program still checks that source and destination mints match.
910/// Prefer `TransferChecked` to also validate the caller-supplied mint and decimals.
911/// This legacy builder calls the classic SPL Token program and is feature-gated.
912#[deprecated(
913    since = "0.2.0",
914    note = "use TransferChecked for explicit classic SPL mint and decimals validation"
915)]
916#[cfg(feature = "legacy-token-instructions")]
917pub struct Transfer<'a> {
918    pub from: &'a AccountView<'a>,
919    pub to: &'a AccountView<'a>,
920    pub authority: &'a AccountView<'a>,
921    pub amount: u64,
922}
923
924#[allow(deprecated)]
925#[cfg(feature = "legacy-token-instructions")]
926impl Transfer<'_> {
927    /// Invoke with the authority already transaction-signed. Fails
928    /// fast with `MissingRequiredSignature` if the authority is not
929    /// a signer, before reaching the CPI.
930    #[inline]
931    pub fn invoke(&self) -> ProgramResult {
932        require_authority_signed_direct(self.authority)?;
933        self.emit(&[], &mut Invoke::legacy(&[]))
934    }
935
936    /// Invoke with explicit PDA seeds. Skips the direct-signer
937    /// pre-check; the supplied signer seeds authorize the CPI.
938    #[inline]
939    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
940        self.emit(&[], &mut Invoke::legacy(signers))
941    }
942}
943
944#[allow(deprecated)]
945#[cfg(feature = "legacy-token-instructions")]
946impl<'a, 'x: 'a> TokenInstruction<'a> for Transfer<'x> {
947    #[inline(always)]
948    fn emit(
949        &self,
950        multisig_signers: &[&'a AccountView<'a>],
951        sink: &mut impl TokenSink<'a>,
952    ) -> ProgramResult {
953        let data = encoders::encode_transfer(self.amount);
954
955        let accounts = [
956            InstructionAccount::writable(self.from.address()),
957            InstructionAccount::writable(self.to.address()),
958            InstructionAccount::readonly_signer(self.authority.address()),
959        ];
960        let views = [self.from, self.to, self.authority];
961        sink.emit(
962            &data,
963            accounts,
964            views,
965            &[Trailing::signers(multisig_signers)],
966        )
967    }
968}
969
970#[allow(deprecated)]
971#[cfg(feature = "legacy-token-instructions")]
972token_program_methods!(Transfer, owner = from, authority = authority);
973
974// ---------------------------------------------------------------------
975
976/// Builder for SPL Token MintTo (instruction index 7).
977///
978/// Prefer [`MintToChecked`] for the decimals-verified path.
979#[deprecated(
980    since = "0.2.0",
981    note = "use MintToChecked for explicit classic SPL mint and decimals validation"
982)]
983#[cfg(feature = "legacy-token-instructions")]
984pub struct MintTo<'a> {
985    pub mint: &'a AccountView<'a>,
986    pub account: &'a AccountView<'a>,
987    pub mint_authority: &'a AccountView<'a>,
988    pub amount: u64,
989}
990
991#[allow(deprecated)]
992#[cfg(feature = "legacy-token-instructions")]
993impl MintTo<'_> {
994    #[inline]
995    pub fn invoke(&self) -> ProgramResult {
996        require_authority_signed_direct(self.mint_authority)?;
997        self.emit(&[], &mut Invoke::legacy(&[]))
998    }
999
1000    #[inline]
1001    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1002        self.emit(&[], &mut Invoke::legacy(signers))
1003    }
1004}
1005
1006#[allow(deprecated)]
1007#[cfg(feature = "legacy-token-instructions")]
1008impl<'a, 'x: 'a> TokenInstruction<'a> for MintTo<'x> {
1009    #[inline(always)]
1010    fn emit(
1011        &self,
1012        multisig_signers: &[&'a AccountView<'a>],
1013        sink: &mut impl TokenSink<'a>,
1014    ) -> ProgramResult {
1015        let data = encoders::encode_mint_to(self.amount);
1016
1017        let accounts = [
1018            InstructionAccount::writable(self.mint.address()),
1019            InstructionAccount::writable(self.account.address()),
1020            InstructionAccount::readonly_signer(self.mint_authority.address()),
1021        ];
1022        let views = [self.mint, self.account, self.mint_authority];
1023        sink.emit(
1024            &data,
1025            accounts,
1026            views,
1027            &[Trailing::signers(multisig_signers)],
1028        )
1029    }
1030}
1031
1032#[allow(deprecated)]
1033#[cfg(feature = "legacy-token-instructions")]
1034token_program_methods!(MintTo, owner = mint, authority = mint_authority);
1035
1036// ---------------------------------------------------------------------
1037
1038/// Builder for SPL Token Burn (instruction index 8).
1039///
1040/// Prefer [`BurnChecked`] for the decimals-verified path.
1041#[deprecated(
1042    since = "0.2.0",
1043    note = "use BurnChecked for explicit classic SPL mint and decimals validation"
1044)]
1045#[cfg(feature = "legacy-token-instructions")]
1046pub struct Burn<'a> {
1047    pub account: &'a AccountView<'a>,
1048    pub mint: &'a AccountView<'a>,
1049    pub authority: &'a AccountView<'a>,
1050    pub amount: u64,
1051}
1052
1053#[allow(deprecated)]
1054#[cfg(feature = "legacy-token-instructions")]
1055impl Burn<'_> {
1056    #[inline]
1057    pub fn invoke(&self) -> ProgramResult {
1058        require_authority_signed_direct(self.authority)?;
1059        self.emit(&[], &mut Invoke::legacy(&[]))
1060    }
1061
1062    #[inline]
1063    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1064        self.emit(&[], &mut Invoke::legacy(signers))
1065    }
1066}
1067
1068#[allow(deprecated)]
1069#[cfg(feature = "legacy-token-instructions")]
1070impl<'a, 'x: 'a> TokenInstruction<'a> for Burn<'x> {
1071    #[inline(always)]
1072    fn emit(
1073        &self,
1074        multisig_signers: &[&'a AccountView<'a>],
1075        sink: &mut impl TokenSink<'a>,
1076    ) -> ProgramResult {
1077        let data = encoders::encode_burn(self.amount);
1078
1079        let accounts = [
1080            InstructionAccount::writable(self.account.address()),
1081            InstructionAccount::writable(self.mint.address()),
1082            InstructionAccount::readonly_signer(self.authority.address()),
1083        ];
1084        let views = [self.account, self.mint, self.authority];
1085        sink.emit(
1086            &data,
1087            accounts,
1088            views,
1089            &[Trailing::signers(multisig_signers)],
1090        )
1091    }
1092}
1093
1094#[allow(deprecated)]
1095#[cfg(feature = "legacy-token-instructions")]
1096token_program_methods!(Burn, owner = account, authority = authority);
1097
1098// ---------------------------------------------------------------------
1099
1100/// Builder for SPL Token CloseAccount (instruction index 9).
1101pub struct CloseAccount<'a> {
1102    pub account: &'a AccountView<'a>,
1103    pub destination: &'a AccountView<'a>,
1104    pub authority: &'a AccountView<'a>,
1105}
1106
1107impl CloseAccount<'_> {
1108    #[inline]
1109    pub fn invoke(&self) -> ProgramResult {
1110        require_authority_signed_direct(self.authority)?;
1111        self.invoke_signed(&[])
1112    }
1113
1114    #[inline]
1115    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1116        self.emit(&[], &mut Invoke::legacy(signers))
1117    }
1118
1119    #[inline]
1120    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1121        require_multisig_signers_direct(multisig_signers)?;
1122        self.invoke_signed_multisig(multisig_signers, &[])
1123    }
1124
1125    #[inline]
1126    pub fn invoke_signed_multisig(
1127        &self,
1128        multisig_signers: &[&AccountView<'_>],
1129        signers: &[Signer<'_, '_>],
1130    ) -> ProgramResult {
1131        self.emit(multisig_signers, &mut Invoke::legacy(signers))
1132    }
1133}
1134
1135impl<'a, 'x: 'a> TokenInstruction<'a> for CloseAccount<'x> {
1136    #[inline(always)]
1137    fn emit(
1138        &self,
1139        multisig_signers: &[&'a AccountView<'a>],
1140        sink: &mut impl TokenSink<'a>,
1141    ) -> ProgramResult {
1142        let data = encoders::encode_close_account();
1143        let accounts = [
1144            InstructionAccount::writable(self.account.address()),
1145            InstructionAccount::writable(self.destination.address()),
1146            authority_meta(self.authority, multisig_signers),
1147        ];
1148        let views = [self.account, self.destination, self.authority];
1149        sink.emit(
1150            &data,
1151            accounts,
1152            views,
1153            &[Trailing::signers(multisig_signers)],
1154        )
1155    }
1156}
1157
1158token_program_methods!(CloseAccount, owner = account, authority = authority);
1159
1160// ---------------------------------------------------------------------
1161
1162/// Builder for SPL Token Approve (instruction index 4).
1163///
1164/// Prefer [`ApproveChecked`] for the decimals-verified path.
1165#[deprecated(
1166    since = "0.2.0",
1167    note = "use ApproveChecked for explicit classic SPL mint and decimals validation"
1168)]
1169#[cfg(feature = "legacy-token-instructions")]
1170pub struct Approve<'a> {
1171    pub source: &'a AccountView<'a>,
1172    pub delegate: &'a AccountView<'a>,
1173    pub authority: &'a AccountView<'a>,
1174    pub amount: u64,
1175}
1176
1177#[allow(deprecated)]
1178#[cfg(feature = "legacy-token-instructions")]
1179impl Approve<'_> {
1180    #[inline]
1181    pub fn invoke(&self) -> ProgramResult {
1182        require_authority_signed_direct(self.authority)?;
1183        self.emit(&[], &mut Invoke::legacy(&[]))
1184    }
1185
1186    #[inline]
1187    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1188        self.emit(&[], &mut Invoke::legacy(signers))
1189    }
1190}
1191
1192#[allow(deprecated)]
1193#[cfg(feature = "legacy-token-instructions")]
1194impl<'a, 'x: 'a> TokenInstruction<'a> for Approve<'x> {
1195    #[inline(always)]
1196    fn emit(
1197        &self,
1198        multisig_signers: &[&'a AccountView<'a>],
1199        sink: &mut impl TokenSink<'a>,
1200    ) -> ProgramResult {
1201        let data = encoders::encode_approve(self.amount);
1202
1203        let accounts = [
1204            InstructionAccount::writable(self.source.address()),
1205            InstructionAccount::readonly(self.delegate.address()),
1206            InstructionAccount::readonly_signer(self.authority.address()),
1207        ];
1208        let views = [self.source, self.delegate, self.authority];
1209        sink.emit(
1210            &data,
1211            accounts,
1212            views,
1213            &[Trailing::signers(multisig_signers)],
1214        )
1215    }
1216}
1217
1218#[allow(deprecated)]
1219#[cfg(feature = "legacy-token-instructions")]
1220token_program_methods!(Approve, owner = source, authority = authority);
1221
1222// ---------------------------------------------------------------------
1223
1224/// Builder for SPL Token Revoke (instruction index 5).
1225pub struct Revoke<'a> {
1226    pub source: &'a AccountView<'a>,
1227    pub authority: &'a AccountView<'a>,
1228}
1229
1230impl Revoke<'_> {
1231    #[inline]
1232    pub fn invoke(&self) -> ProgramResult {
1233        require_authority_signed_direct(self.authority)?;
1234        self.invoke_signed(&[])
1235    }
1236
1237    #[inline]
1238    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1239        self.emit(&[], &mut Invoke::legacy(signers))
1240    }
1241
1242    #[inline]
1243    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1244        require_multisig_signers_direct(multisig_signers)?;
1245        self.invoke_signed_multisig(multisig_signers, &[])
1246    }
1247
1248    #[inline]
1249    pub fn invoke_signed_multisig(
1250        &self,
1251        multisig_signers: &[&AccountView<'_>],
1252        signers: &[Signer<'_, '_>],
1253    ) -> ProgramResult {
1254        self.emit(multisig_signers, &mut Invoke::legacy(signers))
1255    }
1256}
1257
1258impl<'a, 'x: 'a> TokenInstruction<'a> for Revoke<'x> {
1259    #[inline(always)]
1260    fn emit(
1261        &self,
1262        multisig_signers: &[&'a AccountView<'a>],
1263        sink: &mut impl TokenSink<'a>,
1264    ) -> ProgramResult {
1265        let data = encoders::encode_revoke();
1266        let accounts = [
1267            InstructionAccount::writable(self.source.address()),
1268            authority_meta(self.authority, multisig_signers),
1269        ];
1270        let views = [self.source, self.authority];
1271        sink.emit(
1272            &data,
1273            accounts,
1274            views,
1275            &[Trailing::signers(multisig_signers)],
1276        )
1277    }
1278}
1279
1280token_program_methods!(Revoke, owner = source, authority = authority);
1281
1282// ---------------------------------------------------------------------
1283//
1284/// Builder for classic SPL Token TransferChecked (instruction index 12).
1285///
1286/// The token program checks the supplied mint and decimals. This builder calls
1287/// `TOKEN_PROGRAM_ID`; it does not dispatch to Token-2022 or resolve transfer
1288/// hooks. Use `invoke_on(TokenProgram::Token2022, ..)` or `invoke_for_owner` for that program
1289/// integrations, supplying hook accounts when required.
1290pub struct TransferChecked<'a> {
1291    pub from: &'a AccountView<'a>,
1292    pub mint: &'a AccountView<'a>,
1293    pub to: &'a AccountView<'a>,
1294    pub authority: &'a AccountView<'a>,
1295    pub amount: u64,
1296    pub decimals: u8,
1297}
1298
1299impl TransferChecked<'_> {
1300    /// Invoke with a transaction-signed authority. Fails fast with
1301    /// `MissingRequiredSignature` before the CPI if the authority
1302    /// is not a signer.
1303    #[inline]
1304    pub fn invoke(&self) -> ProgramResult {
1305        require_authority_signed_direct(self.authority)?;
1306        self.invoke_signed_unchecked(&[])
1307    }
1308
1309    /// Check transaction signer status and require the source's token owner
1310    /// to equal the authority before CPI. This stricter owner path excludes
1311    /// delegated transfers; use `invoke` when the token program should validate
1312    /// a delegate or `invoke_multisig` for an SPL multisig authority.
1313    ///
1314    /// Verifies `self.from`'s `owner` field (SPL TokenAccount bytes
1315    /// `[32..64]`) matches `self.authority.address()`. Returns
1316    /// `ProgramError::IncorrectAuthority` on mismatch.
1317    #[inline]
1318    pub fn invoke_strict(&self) -> ProgramResult {
1319        require_authority_signed_direct(self.authority)?;
1320        require_token_authority(self.from, self.authority)?;
1321        self.invoke_signed_unchecked(&[])
1322    }
1323
1324    /// Invoke with explicit PDA signer seeds. The SPL token program
1325    /// validates mint + decimals regardless of the signer source.
1326    #[inline]
1327    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1328        self.invoke_signed_unchecked(signers)
1329    }
1330
1331    /// Invoke with an SPL multisig owner account plus transaction-signed
1332    /// multisig signer accounts.
1333    #[inline]
1334    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1335        require_multisig_signers_direct(multisig_signers)?;
1336        self.invoke_signed_multisig(multisig_signers, &[])
1337    }
1338
1339    /// Invoke with an SPL multisig owner account and explicit PDA signer seeds.
1340    #[inline]
1341    pub fn invoke_signed_multisig(
1342        &self,
1343        multisig_signers: &[&AccountView<'_>],
1344        signers: &[Signer<'_, '_>],
1345    ) -> ProgramResult {
1346        self.emit(multisig_signers, &mut Invoke::legacy(signers))
1347    }
1348
1349    /// Strict PDA-signed invoke: ownership pre-check (the SPL token
1350    /// program revalidates, but Hopper surfaces a branded error
1351    /// first) then CPI with the supplied signer seeds.
1352    #[inline]
1353    pub fn invoke_signed_strict(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1354        require_token_authority(self.from, self.authority)?;
1355        self.invoke_signed_unchecked(signers)
1356    }
1357
1358    #[inline(always)]
1359    fn invoke_signed_unchecked(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1360        self.emit(&[], &mut Invoke::legacy(signers))
1361    }
1362}
1363
1364impl<'a, 'x: 'a> TokenInstruction<'a> for TransferChecked<'x> {
1365    #[inline(always)]
1366    fn emit(
1367        &self,
1368        multisig_signers: &[&'a AccountView<'a>],
1369        sink: &mut impl TokenSink<'a>,
1370    ) -> ProgramResult {
1371        let data = encoders::encode_transfer_checked(self.amount, self.decimals);
1372
1373        let accounts = [
1374            InstructionAccount::writable(self.from.address()),
1375            InstructionAccount::readonly(self.mint.address()),
1376            InstructionAccount::writable(self.to.address()),
1377            authority_meta(self.authority, multisig_signers),
1378        ];
1379        let views = [self.from, self.mint, self.to, self.authority];
1380        sink.emit(
1381            &data,
1382            accounts,
1383            views,
1384            &[Trailing::signers(multisig_signers)],
1385        )
1386    }
1387}
1388
1389token_program_methods!(TransferChecked, owner = from, authority = authority);
1390
1391// ---------------------------------------------------------------------
1392
1393/// Builder for SPL Token MintToChecked (instruction index 14).
1394///
1395/// Checks decimals through the classic SPL Token program, like [`TransferChecked`].
1396/// For Token-2022, use `invoke_on(TokenProgram::Token2022, ..)` or `invoke_for_owner`.
1397pub struct MintToChecked<'a> {
1398    pub mint: &'a AccountView<'a>,
1399    pub account: &'a AccountView<'a>,
1400    pub mint_authority: &'a AccountView<'a>,
1401    pub amount: u64,
1402    pub decimals: u8,
1403}
1404
1405impl MintToChecked<'_> {
1406    #[inline]
1407    pub fn invoke(&self) -> ProgramResult {
1408        require_authority_signed_direct(self.mint_authority)?;
1409        self.invoke_signed_unchecked(&[])
1410    }
1411
1412    #[inline]
1413    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1414        self.invoke_signed_unchecked(signers)
1415    }
1416
1417    #[inline]
1418    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1419        require_multisig_signers_direct(multisig_signers)?;
1420        self.invoke_signed_multisig(multisig_signers, &[])
1421    }
1422
1423    #[inline]
1424    pub fn invoke_signed_multisig(
1425        &self,
1426        multisig_signers: &[&AccountView<'_>],
1427        signers: &[Signer<'_, '_>],
1428    ) -> ProgramResult {
1429        self.emit(multisig_signers, &mut Invoke::legacy(signers))
1430    }
1431
1432    #[inline(always)]
1433    fn invoke_signed_unchecked(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1434        self.emit(&[], &mut Invoke::legacy(signers))
1435    }
1436}
1437
1438impl<'a, 'x: 'a> TokenInstruction<'a> for MintToChecked<'x> {
1439    #[inline(always)]
1440    fn emit(
1441        &self,
1442        multisig_signers: &[&'a AccountView<'a>],
1443        sink: &mut impl TokenSink<'a>,
1444    ) -> ProgramResult {
1445        let data = encoders::encode_mint_to_checked(self.amount, self.decimals);
1446
1447        let accounts = [
1448            InstructionAccount::writable(self.mint.address()),
1449            InstructionAccount::writable(self.account.address()),
1450            authority_meta(self.mint_authority, multisig_signers),
1451        ];
1452        let views = [self.mint, self.account, self.mint_authority];
1453        sink.emit(
1454            &data,
1455            accounts,
1456            views,
1457            &[Trailing::signers(multisig_signers)],
1458        )
1459    }
1460}
1461
1462token_program_methods!(MintToChecked, owner = mint, authority = mint_authority);
1463
1464// ---------------------------------------------------------------------
1465
1466/// Builder for SPL Token BurnChecked (instruction index 15).
1467///
1468/// Decimals-verified counterpart to the legacy `Burn` builder. Prefer this over
1469/// `Burn` whenever the mint's decimals are known to the caller,
1470/// so the SPL token program can reject a mis-routed call at CPI time.
1471pub struct BurnChecked<'a> {
1472    pub account: &'a AccountView<'a>,
1473    pub mint: &'a AccountView<'a>,
1474    pub authority: &'a AccountView<'a>,
1475    pub amount: u64,
1476    pub decimals: u8,
1477}
1478
1479impl BurnChecked<'_> {
1480    #[inline]
1481    pub fn invoke(&self) -> ProgramResult {
1482        require_authority_signed_direct(self.authority)?;
1483        self.invoke_signed_unchecked(&[])
1484    }
1485
1486    /// Strict invoke: signer pre-check plus token-account ownership
1487    /// verification. See [`TransferChecked::invoke_strict`] for the
1488    /// full rationale.
1489    #[inline]
1490    pub fn invoke_strict(&self) -> ProgramResult {
1491        require_authority_signed_direct(self.authority)?;
1492        require_token_authority(self.account, self.authority)?;
1493        self.invoke_signed_unchecked(&[])
1494    }
1495
1496    #[inline]
1497    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1498        self.invoke_signed_unchecked(signers)
1499    }
1500
1501    #[inline]
1502    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1503        require_multisig_signers_direct(multisig_signers)?;
1504        self.invoke_signed_multisig(multisig_signers, &[])
1505    }
1506
1507    #[inline]
1508    pub fn invoke_signed_multisig(
1509        &self,
1510        multisig_signers: &[&AccountView<'_>],
1511        signers: &[Signer<'_, '_>],
1512    ) -> ProgramResult {
1513        self.emit(multisig_signers, &mut Invoke::legacy(signers))
1514    }
1515
1516    /// Strict PDA-signed invoke. Pre-check the burn-source owner
1517    /// before the CPI so a misrouted signer surfaces a Hopper-branded
1518    /// error instead of an opaque SPL failure.
1519    #[inline]
1520    pub fn invoke_signed_strict(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1521        require_token_authority(self.account, self.authority)?;
1522        self.invoke_signed_unchecked(signers)
1523    }
1524
1525    #[inline(always)]
1526    fn invoke_signed_unchecked(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1527        self.emit(&[], &mut Invoke::legacy(signers))
1528    }
1529}
1530
1531impl<'a, 'x: 'a> TokenInstruction<'a> for BurnChecked<'x> {
1532    #[inline(always)]
1533    fn emit(
1534        &self,
1535        multisig_signers: &[&'a AccountView<'a>],
1536        sink: &mut impl TokenSink<'a>,
1537    ) -> ProgramResult {
1538        let data = encoders::encode_burn_checked(self.amount, self.decimals);
1539
1540        let accounts = [
1541            InstructionAccount::writable(self.account.address()),
1542            InstructionAccount::writable(self.mint.address()),
1543            authority_meta(self.authority, multisig_signers),
1544        ];
1545        let views = [self.account, self.mint, self.authority];
1546        sink.emit(
1547            &data,
1548            accounts,
1549            views,
1550            &[Trailing::signers(multisig_signers)],
1551        )
1552    }
1553}
1554
1555token_program_methods!(BurnChecked, owner = account, authority = authority);
1556
1557// ---------------------------------------------------------------------
1558
1559/// Builder for SPL Token ApproveChecked (instruction index 13).
1560///
1561/// Mint + decimals-verified approval. Same safety profile as the
1562/// other `*Checked` variants.
1563pub struct ApproveChecked<'a> {
1564    pub source: &'a AccountView<'a>,
1565    pub mint: &'a AccountView<'a>,
1566    pub delegate: &'a AccountView<'a>,
1567    pub authority: &'a AccountView<'a>,
1568    pub amount: u64,
1569    pub decimals: u8,
1570}
1571
1572impl ApproveChecked<'_> {
1573    #[inline]
1574    pub fn invoke(&self) -> ProgramResult {
1575        require_authority_signed_direct(self.authority)?;
1576        self.invoke_signed_unchecked(&[])
1577    }
1578
1579    /// Strict invoke: signer pre-check plus source-account ownership
1580    /// verification. Ensures the authority granting the approval is
1581    /// actually allowed to do so. See [`TransferChecked::invoke_strict`]
1582    /// for the full rationale.
1583    #[inline]
1584    pub fn invoke_strict(&self) -> ProgramResult {
1585        require_authority_signed_direct(self.authority)?;
1586        require_token_authority(self.source, self.authority)?;
1587        self.invoke_signed_unchecked(&[])
1588    }
1589
1590    #[inline]
1591    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1592        self.invoke_signed_unchecked(signers)
1593    }
1594
1595    #[inline]
1596    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1597        require_multisig_signers_direct(multisig_signers)?;
1598        self.invoke_signed_multisig(multisig_signers, &[])
1599    }
1600
1601    #[inline]
1602    pub fn invoke_signed_multisig(
1603        &self,
1604        multisig_signers: &[&AccountView<'_>],
1605        signers: &[Signer<'_, '_>],
1606    ) -> ProgramResult {
1607        self.emit(multisig_signers, &mut Invoke::legacy(signers))
1608    }
1609
1610    /// Strict PDA-signed invoke. Pre-check the source-account owner
1611    /// before the CPI.
1612    #[inline]
1613    pub fn invoke_signed_strict(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1614        require_token_authority(self.source, self.authority)?;
1615        self.invoke_signed_unchecked(signers)
1616    }
1617
1618    #[inline(always)]
1619    fn invoke_signed_unchecked(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1620        self.emit(&[], &mut Invoke::legacy(signers))
1621    }
1622}
1623
1624impl<'a, 'x: 'a> TokenInstruction<'a> for ApproveChecked<'x> {
1625    #[inline(always)]
1626    fn emit(
1627        &self,
1628        multisig_signers: &[&'a AccountView<'a>],
1629        sink: &mut impl TokenSink<'a>,
1630    ) -> ProgramResult {
1631        let data = encoders::encode_approve_checked(self.amount, self.decimals);
1632
1633        let accounts = [
1634            InstructionAccount::writable(self.source.address()),
1635            InstructionAccount::readonly(self.mint.address()),
1636            InstructionAccount::readonly(self.delegate.address()),
1637            authority_meta(self.authority, multisig_signers),
1638        ];
1639        let views = [self.source, self.mint, self.delegate, self.authority];
1640        sink.emit(
1641            &data,
1642            accounts,
1643            views,
1644            &[Trailing::signers(multisig_signers)],
1645        )
1646    }
1647}
1648
1649token_program_methods!(ApproveChecked, owner = source, authority = authority);
1650
1651// ---------------------------------------------------------------------
1652
1653/// Authority classes accepted by SPL Token's SetAuthority instruction.
1654#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1655#[repr(u8)]
1656pub enum TokenAuthorityType {
1657    MintTokens = 0,
1658    FreezeAccount = 1,
1659    AccountOwner = 2,
1660    CloseAccount = 3,
1661}
1662
1663/// Builder for SPL Token SetAuthority (instruction index 6).
1664pub struct SetAuthority<'a> {
1665    pub account: &'a AccountView<'a>,
1666    pub current_authority: &'a AccountView<'a>,
1667    pub authority_type: TokenAuthorityType,
1668    pub new_authority: Option<&'a Address>,
1669}
1670
1671impl SetAuthority<'_> {
1672    #[inline]
1673    pub fn invoke(&self) -> ProgramResult {
1674        require_authority_signed_direct(self.current_authority)?;
1675        self.emit(&[], &mut Invoke::legacy(&[]))
1676    }
1677
1678    #[inline]
1679    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1680        self.emit(&[], &mut Invoke::legacy(signers))
1681    }
1682
1683    #[inline]
1684    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1685        require_multisig_signers_direct(multisig_signers)?;
1686        self.invoke_signed_multisig(multisig_signers, &[])
1687    }
1688
1689    #[inline]
1690    pub fn invoke_signed_multisig(
1691        &self,
1692        multisig_signers: &[&AccountView<'_>],
1693        signers: &[Signer<'_, '_>],
1694    ) -> ProgramResult {
1695        self.emit(multisig_signers, &mut Invoke::legacy(signers))
1696    }
1697}
1698
1699impl<'a, 'x: 'a> TokenInstruction<'a> for SetAuthority<'x> {
1700    #[inline(always)]
1701    fn emit(
1702        &self,
1703        multisig_signers: &[&'a AccountView<'a>],
1704        sink: &mut impl TokenSink<'a>,
1705    ) -> ProgramResult {
1706        let (data, len) = encoders::encode_set_authority(
1707            self.authority_type as u8,
1708            self.new_authority.map(|a| a.as_array()),
1709        );
1710        let accounts = [
1711            InstructionAccount::writable(self.account.address()),
1712            authority_meta(self.current_authority, multisig_signers),
1713        ];
1714        let views = [self.account, self.current_authority];
1715        sink.emit(
1716            &data[..len],
1717            accounts,
1718            views,
1719            &[Trailing::signers(multisig_signers)],
1720        )
1721    }
1722}
1723
1724token_program_methods!(SetAuthority, owner = account, authority = current_authority);
1725
1726// ---------------------------------------------------------------------
1727
1728/// Builder for SPL Token FreezeAccount (instruction index 10).
1729pub struct FreezeAccount<'a> {
1730    pub account: &'a AccountView<'a>,
1731    pub mint: &'a AccountView<'a>,
1732    pub freeze_authority: &'a AccountView<'a>,
1733}
1734
1735impl FreezeAccount<'_> {
1736    #[inline]
1737    pub fn invoke(&self) -> ProgramResult {
1738        require_authority_signed_direct(self.freeze_authority)?;
1739        self.emit(&[], &mut Invoke::legacy(&[]))
1740    }
1741
1742    #[inline]
1743    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1744        self.emit(&[], &mut Invoke::legacy(signers))
1745    }
1746
1747    #[inline]
1748    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1749        require_multisig_signers_direct(multisig_signers)?;
1750        self.invoke_signed_multisig(multisig_signers, &[])
1751    }
1752
1753    #[inline]
1754    pub fn invoke_signed_multisig(
1755        &self,
1756        multisig_signers: &[&AccountView<'_>],
1757        signers: &[Signer<'_, '_>],
1758    ) -> ProgramResult {
1759        self.emit(multisig_signers, &mut Invoke::legacy(signers))
1760    }
1761}
1762
1763impl<'a, 'x: 'a> TokenInstruction<'a> for FreezeAccount<'x> {
1764    #[inline(always)]
1765    fn emit(
1766        &self,
1767        multisig_signers: &[&'a AccountView<'a>],
1768        sink: &mut impl TokenSink<'a>,
1769    ) -> ProgramResult {
1770        let data = encoders::encode_freeze_account();
1771        let accounts = [
1772            InstructionAccount::writable(self.account.address()),
1773            InstructionAccount::readonly(self.mint.address()),
1774            authority_meta(self.freeze_authority, multisig_signers),
1775        ];
1776        let views = [self.account, self.mint, self.freeze_authority];
1777        sink.emit(
1778            &data,
1779            accounts,
1780            views,
1781            &[Trailing::signers(multisig_signers)],
1782        )
1783    }
1784}
1785
1786token_program_methods!(FreezeAccount, owner = account, authority = freeze_authority);
1787
1788/// Builder for SPL Token ThawAccount (instruction index 11).
1789pub struct ThawAccount<'a> {
1790    pub account: &'a AccountView<'a>,
1791    pub mint: &'a AccountView<'a>,
1792    pub freeze_authority: &'a AccountView<'a>,
1793}
1794
1795impl ThawAccount<'_> {
1796    #[inline]
1797    pub fn invoke(&self) -> ProgramResult {
1798        require_authority_signed_direct(self.freeze_authority)?;
1799        self.emit(&[], &mut Invoke::legacy(&[]))
1800    }
1801
1802    #[inline]
1803    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1804        self.emit(&[], &mut Invoke::legacy(signers))
1805    }
1806
1807    #[inline]
1808    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1809        require_multisig_signers_direct(multisig_signers)?;
1810        self.invoke_signed_multisig(multisig_signers, &[])
1811    }
1812
1813    #[inline]
1814    pub fn invoke_signed_multisig(
1815        &self,
1816        multisig_signers: &[&AccountView<'_>],
1817        signers: &[Signer<'_, '_>],
1818    ) -> ProgramResult {
1819        self.emit(multisig_signers, &mut Invoke::legacy(signers))
1820    }
1821}
1822
1823impl<'a, 'x: 'a> TokenInstruction<'a> for ThawAccount<'x> {
1824    #[inline(always)]
1825    fn emit(
1826        &self,
1827        multisig_signers: &[&'a AccountView<'a>],
1828        sink: &mut impl TokenSink<'a>,
1829    ) -> ProgramResult {
1830        let data = encoders::encode_thaw_account();
1831        let accounts = [
1832            InstructionAccount::writable(self.account.address()),
1833            InstructionAccount::readonly(self.mint.address()),
1834            authority_meta(self.freeze_authority, multisig_signers),
1835        ];
1836        let views = [self.account, self.mint, self.freeze_authority];
1837        sink.emit(
1838            &data,
1839            accounts,
1840            views,
1841            &[Trailing::signers(multisig_signers)],
1842        )
1843    }
1844}
1845
1846token_program_methods!(ThawAccount, owner = account, authority = freeze_authority);
1847
1848// ---------------------------------------------------------------------
1849
1850/// Builder for SPL Token SyncNative (instruction index 17).
1851pub struct SyncNative<'a> {
1852    pub account: &'a AccountView<'a>,
1853}
1854
1855impl SyncNative<'_> {
1856    #[inline]
1857    pub fn invoke(&self) -> ProgramResult {
1858        self.emit(&[], &mut Invoke::legacy(&[]))
1859    }
1860}
1861
1862impl<'a, 'x: 'a> TokenInstruction<'a> for SyncNative<'x> {
1863    #[inline(always)]
1864    fn emit(
1865        &self,
1866        multisig_signers: &[&'a AccountView<'a>],
1867        sink: &mut impl TokenSink<'a>,
1868    ) -> ProgramResult {
1869        let data = encoders::encode_sync_native();
1870        let accounts = [InstructionAccount::writable(self.account.address())];
1871        let views = [self.account];
1872        sink.emit(
1873            &data,
1874            accounts,
1875            views,
1876            &[Trailing::signers(multisig_signers)],
1877        )
1878    }
1879}
1880
1881token_program_methods!(SyncNative, owner = account);
1882
1883// ---------------------------------------------------------------------
1884
1885/// Builder for SPL Token InitializeAccount (instruction index 1).
1886pub struct InitializeAccount<'a> {
1887    pub account: &'a AccountView<'a>,
1888    pub mint: &'a AccountView<'a>,
1889    pub owner: &'a AccountView<'a>,
1890    pub rent_sysvar: &'a AccountView<'a>,
1891}
1892
1893impl InitializeAccount<'_> {
1894    #[inline]
1895    pub fn invoke(&self) -> ProgramResult {
1896        self.emit(&[], &mut Invoke::legacy(&[]))
1897    }
1898}
1899
1900impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeAccount<'x> {
1901    #[inline(always)]
1902    fn emit(
1903        &self,
1904        multisig_signers: &[&'a AccountView<'a>],
1905        sink: &mut impl TokenSink<'a>,
1906    ) -> ProgramResult {
1907        let data = encoders::encode_initialize_account();
1908        let accounts = [
1909            InstructionAccount::writable(self.account.address()),
1910            InstructionAccount::readonly(self.mint.address()),
1911            InstructionAccount::readonly(self.owner.address()),
1912            InstructionAccount::readonly(self.rent_sysvar.address()),
1913        ];
1914        let views = [self.account, self.mint, self.owner, self.rent_sysvar];
1915        sink.emit(
1916            &data,
1917            accounts,
1918            views,
1919            &[Trailing::signers(multisig_signers)],
1920        )
1921    }
1922}
1923
1924token_program_methods!(InitializeAccount, owner = account);
1925
1926/// Builder for SPL Token InitializeAccount2 (instruction index 16).
1927pub struct InitializeAccount2<'a> {
1928    pub account: &'a AccountView<'a>,
1929    pub mint: &'a AccountView<'a>,
1930    pub owner: &'a Address,
1931    pub rent_sysvar: &'a AccountView<'a>,
1932}
1933
1934impl InitializeAccount2<'_> {
1935    #[inline]
1936    pub fn invoke(&self) -> ProgramResult {
1937        self.emit(&[], &mut Invoke::legacy(&[]))
1938    }
1939}
1940
1941impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeAccount2<'x> {
1942    #[inline(always)]
1943    fn emit(
1944        &self,
1945        multisig_signers: &[&'a AccountView<'a>],
1946        sink: &mut impl TokenSink<'a>,
1947    ) -> ProgramResult {
1948        let data = encoders::encode_initialize_account_with_owner(16, self.owner.as_array());
1949        let accounts = [
1950            InstructionAccount::writable(self.account.address()),
1951            InstructionAccount::readonly(self.mint.address()),
1952            InstructionAccount::readonly(self.rent_sysvar.address()),
1953        ];
1954        let views = [self.account, self.mint, self.rent_sysvar];
1955        sink.emit(
1956            &data,
1957            accounts,
1958            views,
1959            &[Trailing::signers(multisig_signers)],
1960        )
1961    }
1962}
1963
1964token_program_methods!(InitializeAccount2, owner = account);
1965
1966/// Builder for SPL Token InitializeAccount3 (instruction index 18).
1967pub struct InitializeAccount3<'a> {
1968    pub account: &'a AccountView<'a>,
1969    pub mint: &'a AccountView<'a>,
1970    pub owner: &'a Address,
1971}
1972
1973impl InitializeAccount3<'_> {
1974    #[inline]
1975    pub fn invoke(&self) -> ProgramResult {
1976        self.emit(&[], &mut Invoke::legacy(&[]))
1977    }
1978}
1979
1980impl<'a, 'x: 'a> TokenInstruction<'a> for InitializeAccount3<'x> {
1981    #[inline(always)]
1982    fn emit(
1983        &self,
1984        multisig_signers: &[&'a AccountView<'a>],
1985        sink: &mut impl TokenSink<'a>,
1986    ) -> ProgramResult {
1987        let data = encoders::encode_initialize_account_with_owner(18, self.owner.as_array());
1988        let accounts = [
1989            InstructionAccount::writable(self.account.address()),
1990            InstructionAccount::readonly(self.mint.address()),
1991        ];
1992        let views = [self.account, self.mint];
1993        sink.emit(
1994            &data,
1995            accounts,
1996            views,
1997            &[Trailing::signers(multisig_signers)],
1998        )
1999    }
2000}
2001
2002token_program_methods!(InitializeAccount3, owner = account);
2003
2004/// SPL Token program address.
2005pub const TOKEN_PROGRAM_ID: Address = Address::new_from_array(crate::__decode_base58_32(
2006    "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
2007));
2008
2009// ---------------------------------------------------------------------
2010
2011pub const SPL_TOKEN_ACCOUNT_LEN: usize = 165;
2012pub const SPL_MINT_LEN: usize = 82;
2013
2014const TOKEN_ACCOUNT_MINT_OFFSET: usize = 0;
2015const TOKEN_ACCOUNT_AUTHORITY_OFFSET: usize = 32;
2016const TOKEN_ACCOUNT_AMOUNT_OFFSET: usize = 64;
2017const TOKEN_ACCOUNT_STATE_OFFSET: usize = 108;
2018
2019const MINT_AUTHORITY_TAG_OFFSET: usize = 0;
2020const MINT_AUTHORITY_OFFSET: usize = 4;
2021const MINT_SUPPLY_OFFSET: usize = 36;
2022const MINT_DECIMALS_OFFSET: usize = 44;
2023const MINT_INITIALIZED_OFFSET: usize = 45;
2024const MINT_FREEZE_AUTHORITY_TAG_OFFSET: usize = 46;
2025const MINT_FREEZE_AUTHORITY_OFFSET: usize = 50;
2026
2027/// Known external SPL TokenAccount adapter.
2028pub struct SplTokenAccount;
2029
2030/// Guard-owned zero-copy SPL TokenAccount view.
2031pub struct SplTokenAccountView<'a> {
2032    data: Ref<'a, [u8]>,
2033}
2034
2035impl SplTokenAccountView<'_> {
2036    #[inline(always)]
2037    pub fn mint(&self) -> Address {
2038        read_address_unchecked(&self.data, TOKEN_ACCOUNT_MINT_OFFSET)
2039    }
2040
2041    #[inline(always)]
2042    pub fn authority(&self) -> Address {
2043        read_address_unchecked(&self.data, TOKEN_ACCOUNT_AUTHORITY_OFFSET)
2044    }
2045
2046    #[inline(always)]
2047    pub fn amount(&self) -> u64 {
2048        read_u64_unchecked(&self.data, TOKEN_ACCOUNT_AMOUNT_OFFSET)
2049    }
2050
2051    #[inline(always)]
2052    pub fn state(&self) -> u8 {
2053        self.data[TOKEN_ACCOUNT_STATE_OFFSET]
2054    }
2055
2056    #[inline(always)]
2057    pub fn is_initialized(&self) -> bool {
2058        self.state() != 0
2059    }
2060}
2061
2062impl ExternalZeroCopy for SplTokenAccount {
2063    type View<'a> = SplTokenAccountView<'a>;
2064
2065    const OWNER: Option<Address> = Some(TOKEN_PROGRAM_ID);
2066    const MIN_LEN: usize = SPL_TOKEN_ACCOUNT_LEN;
2067
2068    #[inline]
2069    fn view<'a>(data: Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError> {
2070        Ok(SplTokenAccountView { data })
2071    }
2072}
2073
2074impl ExplainExternal for SplTokenAccount {
2075    fn explain<S: ExternalExplainSink>(account: &AccountView<'_>, sink: &mut S) -> ProgramResult {
2076        let account = ExternalAccount::<SplTokenAccount>::try_new(account)?;
2077        account.with_view(|token| {
2078            sink.field_str("adapter", "SplTokenAccount")?;
2079            sink.field_address("mint", &token.mint())?;
2080            sink.field_address("authority", &token.authority())?;
2081            sink.field_u64("amount", token.amount())?;
2082            sink.field_bool("initialized", token.is_initialized())
2083        })
2084    }
2085}
2086
2087/// Known external SPL Mint adapter.
2088pub struct SplMint;
2089
2090/// Guard-owned zero-copy SPL Mint view.
2091pub struct SplMintView<'a> {
2092    data: Ref<'a, [u8]>,
2093}
2094
2095impl SplMintView<'_> {
2096    #[inline(always)]
2097    pub fn mint_authority(&self) -> Option<Address> {
2098        read_coption_address(&self.data, MINT_AUTHORITY_TAG_OFFSET, MINT_AUTHORITY_OFFSET)
2099    }
2100
2101    #[inline(always)]
2102    pub fn supply(&self) -> u64 {
2103        read_u64_unchecked(&self.data, MINT_SUPPLY_OFFSET)
2104    }
2105
2106    #[inline(always)]
2107    pub fn decimals(&self) -> u8 {
2108        self.data[MINT_DECIMALS_OFFSET]
2109    }
2110
2111    #[inline(always)]
2112    pub fn is_initialized(&self) -> bool {
2113        self.data[MINT_INITIALIZED_OFFSET] != 0
2114    }
2115
2116    #[inline(always)]
2117    pub fn freeze_authority(&self) -> Option<Address> {
2118        read_coption_address(
2119            &self.data,
2120            MINT_FREEZE_AUTHORITY_TAG_OFFSET,
2121            MINT_FREEZE_AUTHORITY_OFFSET,
2122        )
2123    }
2124}
2125
2126impl ExternalZeroCopy for SplMint {
2127    type View<'a> = SplMintView<'a>;
2128
2129    const OWNER: Option<Address> = Some(TOKEN_PROGRAM_ID);
2130    const MIN_LEN: usize = SPL_MINT_LEN;
2131
2132    #[inline]
2133    fn view<'a>(data: Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError> {
2134        Ok(SplMintView { data })
2135    }
2136}
2137
2138impl ExplainExternal for SplMint {
2139    fn explain<S: ExternalExplainSink>(account: &AccountView<'_>, sink: &mut S) -> ProgramResult {
2140        let account = ExternalAccount::<SplMint>::try_new(account)?;
2141        account.with_view(|mint| {
2142            sink.field_str("adapter", "SplMint")?;
2143            sink.field_u64("supply", mint.supply())?;
2144            sink.field_u64("decimals", mint.decimals() as u64)?;
2145            sink.field_bool("initialized", mint.is_initialized())
2146        })
2147    }
2148}
2149
2150/// Proof token that an SPL TokenAccount matched an expected mint.
2151#[derive(Debug)]
2152pub struct CheckedTokenMint<'info> {
2153    account: ExternalAccount<'info, SplTokenAccount>,
2154    mint: Address,
2155}
2156
2157impl<'info> CheckedTokenMint<'info> {
2158    #[inline(always)]
2159    pub const fn account(&self) -> ExternalAccount<'info, SplTokenAccount> {
2160        self.account
2161    }
2162
2163    #[inline(always)]
2164    pub const fn mint(&self) -> Address {
2165        self.mint
2166    }
2167}
2168
2169/// Proof token that an SPL TokenAccount matched an expected token authority.
2170#[derive(Debug)]
2171pub struct CheckedTokenAuthority<'info> {
2172    account: ExternalAccount<'info, SplTokenAccount>,
2173    authority: Address,
2174}
2175
2176impl<'info> CheckedTokenAuthority<'info> {
2177    #[inline(always)]
2178    pub const fn account(&self) -> ExternalAccount<'info, SplTokenAccount> {
2179        self.account
2180    }
2181
2182    #[inline(always)]
2183    pub const fn authority(&self) -> Address {
2184        self.authority
2185    }
2186}
2187
2188/// Proof token that an SPL Mint matched expected decimals.
2189#[derive(Debug)]
2190pub struct CheckedMintDecimals<'info> {
2191    account: ExternalAccount<'info, SplMint>,
2192    decimals: u8,
2193}
2194
2195impl<'info> CheckedMintDecimals<'info> {
2196    #[inline(always)]
2197    pub const fn account(&self) -> ExternalAccount<'info, SplMint> {
2198        self.account
2199    }
2200
2201    #[inline(always)]
2202    pub const fn decimals(&self) -> u8 {
2203        self.decimals
2204    }
2205}
2206
2207/// Snapshot of a token account amount before CPI.
2208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2209pub struct TokenAmountSnapshot {
2210    amount: u64,
2211}
2212
2213impl TokenAmountSnapshot {
2214    #[inline(always)]
2215    pub const fn amount(self) -> u64 {
2216        self.amount
2217    }
2218}
2219
2220impl<'info> ExternalAccount<'info, SplTokenAccount> {
2221    #[inline]
2222    pub fn token_amount(&self) -> Result<u64, ProgramError> {
2223        Ok(self.view()?.amount())
2224    }
2225
2226    #[inline]
2227    pub fn checked_mint(
2228        &self,
2229        expected_mint: &Address,
2230    ) -> Result<CheckedTokenMint<'info>, ProgramError> {
2231        let mint = self.view()?.mint();
2232        if &mint == expected_mint {
2233            Ok(CheckedTokenMint {
2234                account: *self,
2235                mint,
2236            })
2237        } else {
2238            Err(ProgramError::InvalidAccountData)
2239        }
2240    }
2241
2242    #[inline]
2243    pub fn checked_authority(
2244        &self,
2245        expected_authority: &Address,
2246    ) -> Result<CheckedTokenAuthority<'info>, ProgramError> {
2247        let authority = self.view()?.authority();
2248        if &authority == expected_authority {
2249            Ok(CheckedTokenAuthority {
2250                account: *self,
2251                authority,
2252            })
2253        } else {
2254            Err(ProgramError::IncorrectAuthority)
2255        }
2256    }
2257
2258    #[inline]
2259    pub fn amount_snapshot(&self) -> Result<TokenAmountSnapshot, ProgramError> {
2260        Ok(TokenAmountSnapshot {
2261            amount: self.token_amount()?,
2262        })
2263    }
2264
2265    #[inline]
2266    pub fn assert_amount_delta(
2267        &self,
2268        before: TokenAmountSnapshot,
2269        expected_delta: i128,
2270    ) -> ProgramResult {
2271        let after = self.token_amount()? as i128;
2272        let expected = (before.amount as i128)
2273            .checked_add(expected_delta)
2274            .ok_or(ProgramError::ArithmeticOverflow)?;
2275        if expected < 0 || expected > u64::MAX as i128 {
2276            return Err(ProgramError::ArithmeticOverflow);
2277        }
2278        if after == expected {
2279            Ok(())
2280        } else {
2281            Err(ProgramError::InvalidAccountData)
2282        }
2283    }
2284
2285    #[inline]
2286    pub fn assert_amount_unchanged(&self, before: TokenAmountSnapshot) -> ProgramResult {
2287        self.assert_amount_delta(before, 0)
2288    }
2289}
2290
2291impl<'info> ExternalAccount<'info, SplMint> {
2292    #[inline]
2293    pub fn checked_decimals(
2294        &self,
2295        expected: u8,
2296    ) -> Result<CheckedMintDecimals<'info>, ProgramError> {
2297        let decimals = self.view()?.decimals();
2298        if decimals == expected {
2299            Ok(CheckedMintDecimals {
2300                account: *self,
2301                decimals,
2302            })
2303        } else {
2304            Err(ProgramError::InvalidAccountData)
2305        }
2306    }
2307}
2308
2309#[inline(always)]
2310fn read_address_unchecked(data: &[u8], offset: usize) -> Address {
2311    let mut bytes = [0u8; 32];
2312    bytes.copy_from_slice(&data[offset..offset + 32]);
2313    Address::new_from_array(bytes)
2314}
2315
2316#[inline(always)]
2317fn read_u64_unchecked(data: &[u8], offset: usize) -> u64 {
2318    u64::from_le_bytes([
2319        data[offset],
2320        data[offset + 1],
2321        data[offset + 2],
2322        data[offset + 3],
2323        data[offset + 4],
2324        data[offset + 5],
2325        data[offset + 6],
2326        data[offset + 7],
2327    ])
2328}
2329
2330#[inline(always)]
2331fn read_u32_unchecked(data: &[u8], offset: usize) -> u32 {
2332    u32::from_le_bytes([
2333        data[offset],
2334        data[offset + 1],
2335        data[offset + 2],
2336        data[offset + 3],
2337    ])
2338}
2339
2340#[inline(always)]
2341fn read_coption_address(data: &[u8], tag_offset: usize, address_offset: usize) -> Option<Address> {
2342    match read_u32_unchecked(data, tag_offset) {
2343        1 => Some(read_address_unchecked(data, address_offset)),
2344        _ => None,
2345    }
2346}
2347
2348/// Legacy module-path re-exports.
2349pub mod instructions {
2350    pub use super::{
2351        AmountToUiAmount, ApproveChecked, BurnChecked, CloseAccount, FreezeAccount,
2352        GetAccountDataSize, InitializeAccount, InitializeAccount2, InitializeAccount3,
2353        InitializeImmutableOwner, InitializeMint, InitializeMultisig, InitializeMultisig2,
2354        MintToChecked, Revoke, SetAuthority, SyncNative, ThawAccount, TokenAuthorityType,
2355        TokenBatch, TokenInstruction, TokenProgram, TransferChecked, UiAmountToAmount,
2356        UnwrapLamports, WithdrawExcessLamports,
2357    };
2358
2359    #[cfg(feature = "legacy-token-instructions")]
2360    #[allow(deprecated)]
2361    pub use super::{Approve, Burn, MintTo, Transfer};
2362}
2363
2364#[cfg(test)]
2365mod tests {
2366    //! Wire-format regression tests for the builder instruction-data.
2367    //!
2368    //! The SPL token program decodes every instruction by its first
2369    //! byte, so getting the discriminator wrong silently routes to
2370    //! a different op. These tests lock the exact byte layout each
2371    //! builder produces.
2372
2373    use super::*;
2374    use hopper_native::{
2375        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
2376    };
2377    fn make_account(owner: Address, data: &[u8]) -> (std::vec::Vec<u64>, AccountView<'static>) {
2378        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + data.len()).div_ceil(8)];
2379        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
2380        // SAFETY: Test helper writes a valid RuntimeAccount header and copies
2381        // payload bytes into owned backing memory.
2382        unsafe {
2383            raw.write(RuntimeAccount {
2384                borrow_state: NOT_BORROWED,
2385                is_signer: 0,
2386                is_writable: 1,
2387                executable: 0,
2388                resize_delta: 0,
2389                address: NativeAddress::new_from_array([7; 32]),
2390                owner: NativeAddress::new_from_array(owner.to_bytes()),
2391                lamports: 1,
2392                data_len: data.len() as u64,
2393            });
2394            let data_ptr = (backing.as_mut_ptr() as *mut u8).add(RuntimeAccount::SIZE);
2395            core::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, data.len());
2396        }
2397        // SAFETY: `raw` points at the initialized RuntimeAccount header.
2398        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
2399        (backing, AccountView::from_backend(backend))
2400    }
2401    fn token_account_data(
2402        mint: Address,
2403        authority: Address,
2404        amount: u64,
2405    ) -> [u8; SPL_TOKEN_ACCOUNT_LEN] {
2406        let mut data = [0u8; SPL_TOKEN_ACCOUNT_LEN];
2407        data[0..32].copy_from_slice(mint.as_bytes());
2408        data[32..64].copy_from_slice(authority.as_bytes());
2409        data[64..72].copy_from_slice(&amount.to_le_bytes());
2410        data[108] = 1;
2411        data
2412    }
2413    fn mint_data(authority: Address, supply: u64, decimals: u8) -> [u8; SPL_MINT_LEN] {
2414        let mut data = [0u8; SPL_MINT_LEN];
2415        data[0..4].copy_from_slice(&1u32.to_le_bytes());
2416        data[4..36].copy_from_slice(authority.as_bytes());
2417        data[36..44].copy_from_slice(&supply.to_le_bytes());
2418        data[44] = decimals;
2419        data[45] = 1;
2420        data
2421    }
2422
2423    // Verify the discriminator byte of each `*Checked` variant
2424    // matches the SPL Token program's public definition. These are
2425    // stability tests: if SPL ever renumbered indices the builder
2426    // would silently route to the wrong instruction without them.
2427    #[test]
2428    fn transfer_checked_discriminator_is_12() {
2429        // The SPL Token program's instruction enum assigns:
2430        //   0 = InitializeMint
2431        //   3 = Transfer
2432        //  12 = TransferChecked
2433        //  13 = ApproveChecked
2434        //  14 = MintToChecked
2435        //  15 = BurnChecked
2436        // We assert each builder hard-codes the right index.
2437        //
2438        // We can't instantiate a builder without an `AccountView`,
2439        // but we can read the constant directly from the source by
2440        // looking at the first byte the `invoke_signed_unchecked`
2441        // writes. Expressing that here as a documentation-level
2442        // contract, the wire-format tests below build a real data
2443        // buffer and lock the discriminator there.
2444        //
2445        // Keep these tests if the SPL Token program adds new
2446        // instructions that might conflict; they pin our build to
2447        // the canonical numbering.
2448    }
2449    #[test]
2450    fn spl_external_token_account_view_proofs_and_amount_delta() {
2451        let mint = Address::new_from_array([2; 32]);
2452        let authority = Address::new_from_array([3; 32]);
2453        let data = token_account_data(mint, authority, 100);
2454        let (mut backing, account) = make_account(TOKEN_PROGRAM_ID, &data);
2455
2456        let token = ExternalAccount::<SplTokenAccount>::try_new(&account).unwrap();
2457        let view = token.view().unwrap();
2458        assert_eq!(view.mint(), mint);
2459        assert_eq!(view.authority(), authority);
2460        assert_eq!(view.amount(), 100);
2461        assert!(view.is_initialized());
2462        assert_eq!(token.checked_mint(&mint).unwrap().mint(), mint);
2463        assert_eq!(
2464            token.checked_authority(&authority).unwrap().authority(),
2465            authority
2466        );
2467        assert_eq!(
2468            token
2469                .checked_mint(&Address::new_from_array([9; 32]))
2470                .unwrap_err(),
2471            ProgramError::InvalidAccountData
2472        );
2473
2474        let before = token.amount_snapshot().unwrap();
2475        // Byte view over the word-aligned backing (the fixture keeps the
2476        // allocation 8-aligned for the RuntimeAccount header).
2477        // SAFETY: `backing` owns these bytes; u8 has no alignment demands.
2478        let backing_bytes = unsafe {
2479            core::slice::from_raw_parts_mut(backing.as_mut_ptr() as *mut u8, backing.len() * 8)
2480        };
2481        backing_bytes[RuntimeAccount::SIZE + 64..RuntimeAccount::SIZE + 72]
2482            .copy_from_slice(&150u64.to_le_bytes());
2483        token.assert_amount_delta(before, 50).unwrap();
2484        assert_eq!(
2485            token.assert_amount_delta(before, 49).unwrap_err(),
2486            ProgramError::InvalidAccountData
2487        );
2488    }
2489    #[test]
2490    fn spl_external_mint_view_and_decimals_proof() {
2491        let authority = Address::new_from_array([4; 32]);
2492        let data = mint_data(authority, 1_000_000, 6);
2493        let (_backing, account) = make_account(TOKEN_PROGRAM_ID, &data);
2494
2495        let mint = ExternalAccount::<SplMint>::try_new(&account).unwrap();
2496        let view = mint.view().unwrap();
2497        assert_eq!(view.mint_authority(), Some(authority));
2498        assert_eq!(view.supply(), 1_000_000);
2499        assert_eq!(view.decimals(), 6);
2500        assert!(view.is_initialized());
2501        assert_eq!(mint.checked_decimals(6).unwrap().decimals(), 6);
2502        assert_eq!(
2503            mint.checked_decimals(9).unwrap_err(),
2504            ProgramError::InvalidAccountData
2505        );
2506    }
2507
2508    /// Helper: reconstruct the 10-byte instruction-data buffer a
2509    /// `*Checked` builder writes, bypassing the CPI so the test has
2510    /// no AccountView dependency.
2511    fn encode_checked(disc: u8, amount: u64, decimals: u8) -> [u8; 10] {
2512        let mut data = [0u8; 10];
2513        data[0] = disc;
2514        data[1..9].copy_from_slice(&amount.to_le_bytes());
2515        data[9] = decimals;
2516        data
2517    }
2518
2519    #[test]
2520    fn transfer_checked_wire_format_is_stable() {
2521        // 12, amount LE, decimals = [12, a0..a7, dec]
2522        let out = encode_checked(12, 0x0102_0304_0506_0708, 9);
2523        assert_eq!(out[0], 12);
2524        assert_eq!(
2525            &out[1..9],
2526            &[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]
2527        );
2528        assert_eq!(out[9], 9);
2529    }
2530
2531    #[test]
2532    fn mint_to_checked_wire_format_is_stable() {
2533        let out = encode_checked(14, 1000, 6);
2534        assert_eq!(out[0], 14);
2535        assert_eq!(u64::from_le_bytes(out[1..9].try_into().unwrap()), 1000);
2536        assert_eq!(out[9], 6);
2537    }
2538
2539    #[test]
2540    fn burn_checked_wire_format_is_stable() {
2541        let out = encode_checked(15, 42, 8);
2542        assert_eq!(out[0], 15);
2543        assert_eq!(u64::from_le_bytes(out[1..9].try_into().unwrap()), 42);
2544        assert_eq!(out[9], 8);
2545    }
2546
2547    #[test]
2548    fn approve_checked_wire_format_is_stable() {
2549        let out = encode_checked(13, u64::MAX, 0);
2550        assert_eq!(out[0], 13);
2551        assert_eq!(u64::from_le_bytes(out[1..9].try_into().unwrap()), u64::MAX);
2552        assert_eq!(out[9], 0);
2553    }
2554
2555    #[test]
2556    fn checked_encoding_round_trips_decimals_range() {
2557        // 0..=255 decimals must all survive the encode. Some SPL
2558        // mints have decimals > 9 (e.g. native SOL = 9; synthetic
2559        // mints use larger values).
2560        for d in 0u8..=255 {
2561            let out = encode_checked(12, 1, d);
2562            assert_eq!(out[9], d);
2563        }
2564    }
2565
2566    #[test]
2567    fn checked_encoding_preserves_amount_bits() {
2568        // Every byte in the amount field must land at its expected
2569        // little-endian slot.
2570        for shift in 0..8 {
2571            let amount = 0xABu64 << (shift * 8);
2572            let out = encode_checked(12, amount, 0);
2573            let decoded = u64::from_le_bytes(out[1..9].try_into().unwrap());
2574            assert_eq!(decoded, amount);
2575        }
2576    }
2577
2578    #[test]
2579    fn authority_and_initialize_encodings_match_spl_token_wire_format() {
2580        let authority = Address::new_from_array([9; 32]);
2581        let (set_authority, len) = encoders::encode_set_authority(
2582            TokenAuthorityType::AccountOwner as u8,
2583            Some(authority.as_array()),
2584        );
2585        assert_eq!(len, 35);
2586        assert_eq!(set_authority[0], 6);
2587        assert_eq!(set_authority[1], 2);
2588        assert_eq!(set_authority[2], 1);
2589        assert_eq!(&set_authority[3..35], authority.as_bytes());
2590
2591        let (set_authority, len) =
2592            encoders::encode_set_authority(TokenAuthorityType::CloseAccount as u8, None);
2593        assert_eq!(len, 3);
2594        assert_eq!(&set_authority[..3], &[6, 3, 0]);
2595
2596        let init2 = encoders::encode_initialize_account_with_owner(16, authority.as_array());
2597        let init3 = encoders::encode_initialize_account_with_owner(18, authority.as_array());
2598        assert_eq!(init2[0], 16);
2599        assert_eq!(init3[0], 18);
2600        assert_eq!(&init2[1..33], authority.as_bytes());
2601        assert_eq!(&init3[1..33], authority.as_bytes());
2602    }
2603
2604    /// Byte-identity guard for the extracted [`encoders`] module: each
2605    /// shipped encoder must reproduce the exact bytes the builders wrote
2606    /// inline before the refactor. These literals are the pre-refactor wire
2607    /// bytes; if any diverges, a CPI's instruction-data changed.
2608    #[test]
2609    fn shipped_encoders_match_pre_refactor_golden_bytes() {
2610        // amount = 1 → little-endian 01 00 00 00 00 00 00 00.
2611        assert_eq!(encoders::encode_transfer(1), [3, 1, 0, 0, 0, 0, 0, 0, 0]);
2612        assert_eq!(encoders::encode_approve(1), [4, 1, 0, 0, 0, 0, 0, 0, 0]);
2613        assert_eq!(encoders::encode_mint_to(1), [7, 1, 0, 0, 0, 0, 0, 0, 0]);
2614        assert_eq!(encoders::encode_burn(1), [8, 1, 0, 0, 0, 0, 0, 0, 0]);
2615        assert_eq!(
2616            encoders::encode_transfer_checked(1, 9),
2617            [12, 1, 0, 0, 0, 0, 0, 0, 0, 9]
2618        );
2619        assert_eq!(
2620            encoders::encode_approve_checked(1, 9),
2621            [13, 1, 0, 0, 0, 0, 0, 0, 0, 9]
2622        );
2623        assert_eq!(
2624            encoders::encode_mint_to_checked(1, 9),
2625            [14, 1, 0, 0, 0, 0, 0, 0, 0, 9]
2626        );
2627        assert_eq!(
2628            encoders::encode_burn_checked(1, 9),
2629            [15, 1, 0, 0, 0, 0, 0, 0, 0, 9]
2630        );
2631        assert_eq!(encoders::encode_revoke(), [5]);
2632        assert_eq!(encoders::encode_close_account(), [9]);
2633        assert_eq!(encoders::encode_freeze_account(), [10]);
2634        assert_eq!(encoders::encode_thaw_account(), [11]);
2635        assert_eq!(encoders::encode_sync_native(), [17]);
2636        assert_eq!(encoders::encode_initialize_account(), [1]);
2637
2638        let owner = [
2639            0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
2640            24, 25, 26, 27, 28, 29, 30, 31,
2641        ];
2642        let init2 = encoders::encode_initialize_account_with_owner(16, &owner);
2643        assert_eq!(init2[0], 16);
2644        assert_eq!(&init2[1..33], &owner);
2645        let init3 = encoders::encode_initialize_account_with_owner(18, &owner);
2646        assert_eq!(init3[0], 18);
2647        assert_eq!(&init3[1..33], &owner);
2648
2649        let (sa_some, some_len) = encoders::encode_set_authority(2, Some(&owner));
2650        assert_eq!(some_len, 35);
2651        assert_eq!(sa_some[0], 6);
2652        assert_eq!(sa_some[1], 2);
2653        assert_eq!(sa_some[2], 1);
2654        assert_eq!(&sa_some[3..35], &owner);
2655        let (sa_none, none_len) = encoders::encode_set_authority(3, None);
2656        assert_eq!(none_len, 3);
2657        assert_eq!(&sa_none[..3], &[6, 3, 0]);
2658    }
2659
2660    // ---------------------------------------------------------------------
2661
2662    /// Build a minimal valid SPL TokenAccount data buffer + an
2663    /// AccountView wrapping it, plus a matching authority view. The
2664    /// token account's `owner` field (bytes [32..64]) is set to the
2665    /// requested authority so the ownership check passes by default;
2666    /// individual tests can mutate the buffer to exercise mismatch.
2667    fn make_token_and_authority(
2668        authority_bytes: [u8; 32],
2669        token_owner_bytes: [u8; 32],
2670    ) -> (
2671        std::vec::Vec<u64>,
2672        std::vec::Vec<u64>,
2673        crate::account::AccountView<'static>,
2674        crate::account::AccountView<'static>,
2675    ) {
2676        use hopper_native::{
2677            AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
2678            NOT_BORROWED,
2679        };
2680
2681        // TokenAccount: SPL layout is 165 bytes; first 32 bytes are
2682        // `mint`, next 32 are `owner`. We only care about the owner
2683        // slot for `require_token_authority`, but size the buffer at
2684        // 165 so it looks like a real TokenAccount.
2685        let token_data_len = 165;
2686        let mut token_backing =
2687            std::vec![0u64; (RuntimeAccount::SIZE + token_data_len).div_ceil(8)];
2688        let token_raw = token_backing.as_mut_ptr() as *mut RuntimeAccount;
2689        // 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.
2690        unsafe {
2691            token_raw.write(RuntimeAccount {
2692                borrow_state: NOT_BORROWED,
2693                is_signer: 0,
2694                is_writable: 1,
2695                executable: 0,
2696                resize_delta: 0,
2697                address: NativeAddress::new_from_array([0xAA; 32]),
2698                owner: NativeAddress::new_from_array([3; 32]),
2699                lamports: 2_039_280,
2700                data_len: token_data_len as u64,
2701            });
2702            // Write the SPL TokenAccount.owner field at data[32..64].
2703            let data_ptr = (token_raw as *mut u8).add(RuntimeAccount::SIZE);
2704            core::ptr::copy_nonoverlapping(token_owner_bytes.as_ptr(), data_ptr.add(32), 32);
2705        }
2706        // 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.
2707        let token_backend = unsafe { NativeAccountView::new_unchecked(token_raw) };
2708        let token_view = crate::account::AccountView::from_backend(token_backend);
2709
2710        // Authority: no data needed, just an address field.
2711        let mut auth_backing = std::vec![0u64; (RuntimeAccount::SIZE).div_ceil(8)];
2712        let auth_raw = auth_backing.as_mut_ptr() as *mut RuntimeAccount;
2713        // 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.
2714        unsafe {
2715            auth_raw.write(RuntimeAccount {
2716                borrow_state: NOT_BORROWED,
2717                is_signer: 1,
2718                is_writable: 0,
2719                executable: 0,
2720                resize_delta: 0,
2721                address: NativeAddress::new_from_array(authority_bytes),
2722                owner: NativeAddress::new_from_array([0; 32]),
2723                lamports: 0,
2724                data_len: 0,
2725            });
2726        }
2727        // 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.
2728        let auth_backend = unsafe { NativeAccountView::new_unchecked(auth_raw) };
2729        let auth_view = crate::account::AccountView::from_backend(auth_backend);
2730
2731        (token_backing, auth_backing, token_view, auth_view)
2732    }
2733
2734    #[test]
2735    fn require_token_authority_accepts_matching_owner() {
2736        let authority = [0x42u8; 32];
2737        let (_tb, _ab, token, auth) = make_token_and_authority(authority, authority);
2738        require_token_authority(&token, &auth).unwrap();
2739    }
2740
2741    #[test]
2742    fn require_token_authority_rejects_mismatched_owner() {
2743        let authority = [0x42u8; 32];
2744        let wrong_owner = [0x77u8; 32];
2745        let (_tb, _ab, token, auth) = make_token_and_authority(authority, wrong_owner);
2746        let err = require_token_authority(&token, &auth).unwrap_err();
2747        assert!(matches!(err, ProgramError::IncorrectAuthority));
2748    }
2749
2750    #[test]
2751    fn require_token_authority_rejects_short_buffer() {
2752        use hopper_native::{
2753            AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
2754            NOT_BORROWED,
2755        };
2756
2757        // Token account with only 50 bytes of data is not a valid
2758        // SPL TokenAccount (owner field starts at byte 32 and runs
2759        // through byte 63, so a 50-byte buffer is short).
2760        let data_len = 50;
2761        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + data_len).div_ceil(8)];
2762        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
2763        // 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.
2764        unsafe {
2765            raw.write(RuntimeAccount {
2766                borrow_state: NOT_BORROWED,
2767                is_signer: 0,
2768                is_writable: 1,
2769                executable: 0,
2770                resize_delta: 0,
2771                address: NativeAddress::new_from_array([0xAA; 32]),
2772                owner: NativeAddress::new_from_array([3; 32]),
2773                lamports: 0,
2774                data_len: data_len as u64,
2775            });
2776        }
2777        // 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.
2778        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
2779        let token = crate::account::AccountView::from_backend(backend);
2780
2781        let (_ab, _, _, auth) = make_token_and_authority([0x11; 32], [0x11; 32]);
2782        let err = require_token_authority(&token, &auth).unwrap_err();
2783        assert!(matches!(err, ProgramError::AccountDataTooSmall));
2784    }
2785
2786    // ---------------------------------------------------------------------
2787    //
2788    // These lock in the behavior that `#[account(token::mint = X)]`,
2789    // `#[account(mint::authority = Y)]`, and friends lower to. They
2790    // share the same harness as require_token_authority above, but
2791    // exercise different byte ranges of the account buffer.
2792
2793    /// Construct a valid SPL TokenAccount-shaped buffer (165 bytes)
2794    /// with both `mint` (bytes 0..32) and `owner` (bytes 32..64)
2795    /// populated to the caller's choice. Used by the token_mint /
2796    /// token_owner_eq regression tests.
2797    fn make_token_with_mint_and_owner(
2798        mint_bytes: [u8; 32],
2799        owner_bytes: [u8; 32],
2800    ) -> (std::vec::Vec<u64>, crate::account::AccountView<'static>) {
2801        use hopper_native::{
2802            AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
2803            NOT_BORROWED,
2804        };
2805
2806        let token_data_len = 165;
2807        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + token_data_len).div_ceil(8)];
2808        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
2809        // 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.
2810        unsafe {
2811            raw.write(RuntimeAccount {
2812                borrow_state: NOT_BORROWED,
2813                is_signer: 0,
2814                is_writable: 1,
2815                executable: 0,
2816                resize_delta: 0,
2817                address: NativeAddress::new_from_array([0xAA; 32]),
2818                owner: NativeAddress::new_from_array([3; 32]),
2819                lamports: 2_039_280,
2820                data_len: token_data_len as u64,
2821            });
2822            let data_ptr = (raw as *mut u8).add(RuntimeAccount::SIZE);
2823            core::ptr::copy_nonoverlapping(mint_bytes.as_ptr(), data_ptr, 32);
2824            core::ptr::copy_nonoverlapping(owner_bytes.as_ptr(), data_ptr.add(32), 32);
2825        }
2826        // 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.
2827        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
2828        let view = crate::account::AccountView::from_backend(backend);
2829        (backing, view)
2830    }
2831
2832    /// Construct a valid SPL Mint-shaped buffer (82 bytes), with the
2833    /// mint_authority COption set to Some(auth), decimals populated,
2834    /// and the freeze_authority COption left empty (None).
2835    fn make_mint_with_authority_decimals(
2836        mint_authority: [u8; 32],
2837        decimals: u8,
2838    ) -> (std::vec::Vec<u64>, crate::account::AccountView<'static>) {
2839        use hopper_native::{
2840            AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
2841            NOT_BORROWED,
2842        };
2843
2844        let mint_data_len = 82;
2845        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + mint_data_len).div_ceil(8)];
2846        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
2847        // 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.
2848        unsafe {
2849            raw.write(RuntimeAccount {
2850                borrow_state: NOT_BORROWED,
2851                is_signer: 0,
2852                is_writable: 0,
2853                executable: 0,
2854                resize_delta: 0,
2855                address: NativeAddress::new_from_array([0xBB; 32]),
2856                owner: NativeAddress::new_from_array([3; 32]),
2857                lamports: 1_461_600,
2858                data_len: mint_data_len as u64,
2859            });
2860            let data_ptr = (raw as *mut u8).add(RuntimeAccount::SIZE);
2861            // mint_authority COption tag = Some (u32 LE = 1).
2862            let some_tag: [u8; 4] = 1u32.to_le_bytes();
2863            core::ptr::copy_nonoverlapping(some_tag.as_ptr(), data_ptr, 4);
2864            core::ptr::copy_nonoverlapping(mint_authority.as_ptr(), data_ptr.add(4), 32);
2865            // Supply bytes [36..44] stay zero.
2866            // Decimals at byte 44.
2867            *data_ptr.add(44) = decimals;
2868            // is_initialized byte 45 = 1.
2869            *data_ptr.add(45) = 1;
2870            // freeze_authority COption tag = None (bytes 46..50 stay zero).
2871        }
2872        // 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.
2873        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
2874        let view = crate::account::AccountView::from_backend(backend);
2875        (backing, view)
2876    }
2877
2878    #[test]
2879    fn require_token_mint_accepts_matching_mint() {
2880        let mint = [0xABu8; 32];
2881        let (_b, view) = make_token_with_mint_and_owner(mint, [0; 32]);
2882        let expected = crate::address::Address::new_from_array(mint);
2883        require_token_mint(&view, &expected).unwrap();
2884    }
2885
2886    #[test]
2887    fn require_token_mint_rejects_mismatched_mint() {
2888        let mint = [0xABu8; 32];
2889        let (_b, view) = make_token_with_mint_and_owner(mint, [0; 32]);
2890        let wrong = crate::address::Address::new_from_array([0xCDu8; 32]);
2891        let err = require_token_mint(&view, &wrong).unwrap_err();
2892        assert!(matches!(err, ProgramError::InvalidAccountData));
2893    }
2894
2895    #[test]
2896    fn require_token_owner_eq_matches() {
2897        let owner = [0x77u8; 32];
2898        let (_b, view) = make_token_with_mint_and_owner([0; 32], owner);
2899        let expected = crate::address::Address::new_from_array(owner);
2900        require_token_owner_eq(&view, &expected).unwrap();
2901    }
2902
2903    #[test]
2904    fn require_token_owner_eq_rejects_mismatch() {
2905        let owner = [0x77u8; 32];
2906        let (_b, view) = make_token_with_mint_and_owner([0; 32], owner);
2907        let wrong = crate::address::Address::new_from_array([0x88u8; 32]);
2908        let err = require_token_owner_eq(&view, &wrong).unwrap_err();
2909        assert!(matches!(err, ProgramError::IncorrectAuthority));
2910    }
2911
2912    #[test]
2913    fn require_mint_authority_accepts_matching() {
2914        let auth = [0x99u8; 32];
2915        let (_b, view) = make_mint_with_authority_decimals(auth, 6);
2916        let expected = crate::address::Address::new_from_array(auth);
2917        require_mint_authority(&view, &expected).unwrap();
2918    }
2919
2920    #[test]
2921    fn require_mint_authority_rejects_mismatched() {
2922        let auth = [0x99u8; 32];
2923        let (_b, view) = make_mint_with_authority_decimals(auth, 6);
2924        let wrong = crate::address::Address::new_from_array([0x00u8; 32]);
2925        let err = require_mint_authority(&view, &wrong).unwrap_err();
2926        assert!(matches!(err, ProgramError::IncorrectAuthority));
2927    }
2928
2929    #[test]
2930    fn require_mint_decimals_matches() {
2931        let (_b, view) = make_mint_with_authority_decimals([1u8; 32], 9);
2932        require_mint_decimals(&view, 9).unwrap();
2933    }
2934
2935    #[test]
2936    fn require_mint_decimals_rejects_mismatch() {
2937        let (_b, view) = make_mint_with_authority_decimals([1u8; 32], 9);
2938        let err = require_mint_decimals(&view, 6).unwrap_err();
2939        assert!(matches!(err, ProgramError::InvalidAccountData));
2940    }
2941
2942    #[test]
2943    fn require_mint_freeze_authority_rejects_none_tag() {
2944        // `make_mint_with_authority_decimals` deliberately leaves
2945        // freeze_authority as None. asking for a specific freeze
2946        // authority on such a mint must fail with InvalidAccountData
2947        // (not IncorrectAuthority, because the tag is the problem
2948        // rather than the pubkey bytes).
2949        let (_b, view) = make_mint_with_authority_decimals([1u8; 32], 9);
2950        let expected = crate::address::Address::new_from_array([2u8; 32]);
2951        let err = require_mint_freeze_authority(&view, &expected).unwrap_err();
2952        assert!(matches!(err, ProgramError::InvalidAccountData));
2953    }
2954}