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