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 instruction carries neither an explicit mint account nor decimals.
512/// The token program still checks that source and destination mints match.
513/// Prefer `TransferChecked` to also validate the caller-supplied mint and decimals.
514/// This legacy builder calls the classic SPL Token program and is feature-gated.
515#[deprecated(
516    since = "0.2.0",
517    note = "use TransferChecked for explicit classic SPL mint and decimals validation"
518)]
519#[cfg(feature = "legacy-token-instructions")]
520pub struct Transfer<'a> {
521    pub from: &'a AccountView<'a>,
522    pub to: &'a AccountView<'a>,
523    pub authority: &'a AccountView<'a>,
524    pub amount: u64,
525}
526
527#[allow(deprecated)]
528#[cfg(feature = "legacy-token-instructions")]
529impl Transfer<'_> {
530    /// Invoke with the authority already transaction-signed. Fails
531    /// fast with `MissingRequiredSignature` if the authority is not
532    /// a signer, before reaching the CPI.
533    #[inline]
534    pub fn invoke(&self) -> ProgramResult {
535        require_authority_signed_direct(self.authority)?;
536        self.invoke_signed_unchecked(&[])
537    }
538
539    /// Invoke with explicit PDA seeds. Skips the direct-signer
540    /// pre-check; the supplied signer seeds authorize the CPI.
541    #[inline]
542    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
543        self.invoke_signed_unchecked(signers)
544    }
545
546    #[inline(always)]
547    fn invoke_signed_unchecked(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
548        let data = encoders::encode_transfer(self.amount);
549
550        let accounts = [
551            InstructionAccount::writable(self.from.address()),
552            InstructionAccount::writable(self.to.address()),
553            InstructionAccount::readonly_signer(self.authority.address()),
554        ];
555        let views = [self.from, self.to, self.authority];
556        let instruction = InstructionView {
557            program_id: &TOKEN_PROGRAM_ID,
558            data: &data,
559            accounts: &accounts,
560        };
561
562        crate::cpi::invoke_signed(&instruction, &views, signers)
563    }
564}
565
566// ---------------------------------------------------------------------
567
568/// Builder for SPL Token MintTo (instruction index 7).
569///
570/// Prefer [`MintToChecked`] for the decimals-verified path.
571#[deprecated(
572    since = "0.2.0",
573    note = "use MintToChecked for explicit classic SPL mint and decimals validation"
574)]
575#[cfg(feature = "legacy-token-instructions")]
576pub struct MintTo<'a> {
577    pub mint: &'a AccountView<'a>,
578    pub account: &'a AccountView<'a>,
579    pub mint_authority: &'a AccountView<'a>,
580    pub amount: u64,
581}
582
583#[allow(deprecated)]
584#[cfg(feature = "legacy-token-instructions")]
585impl MintTo<'_> {
586    #[inline]
587    pub fn invoke(&self) -> ProgramResult {
588        require_authority_signed_direct(self.mint_authority)?;
589        self.invoke_signed(&[])
590    }
591
592    #[inline]
593    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
594        let data = encoders::encode_mint_to(self.amount);
595
596        let accounts = [
597            InstructionAccount::writable(self.mint.address()),
598            InstructionAccount::writable(self.account.address()),
599            InstructionAccount::readonly_signer(self.mint_authority.address()),
600        ];
601        let views = [self.mint, self.account, self.mint_authority];
602        let instruction = InstructionView {
603            program_id: &TOKEN_PROGRAM_ID,
604            data: &data,
605            accounts: &accounts,
606        };
607
608        crate::cpi::invoke_signed(&instruction, &views, signers)
609    }
610}
611
612// ---------------------------------------------------------------------
613
614/// Builder for SPL Token Burn (instruction index 8).
615///
616/// Prefer [`BurnChecked`] for the decimals-verified path.
617#[deprecated(
618    since = "0.2.0",
619    note = "use BurnChecked for explicit classic SPL mint and decimals validation"
620)]
621#[cfg(feature = "legacy-token-instructions")]
622pub struct Burn<'a> {
623    pub account: &'a AccountView<'a>,
624    pub mint: &'a AccountView<'a>,
625    pub authority: &'a AccountView<'a>,
626    pub amount: u64,
627}
628
629#[allow(deprecated)]
630#[cfg(feature = "legacy-token-instructions")]
631impl Burn<'_> {
632    #[inline]
633    pub fn invoke(&self) -> ProgramResult {
634        require_authority_signed_direct(self.authority)?;
635        self.invoke_signed(&[])
636    }
637
638    #[inline]
639    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
640        let data = encoders::encode_burn(self.amount);
641
642        let accounts = [
643            InstructionAccount::writable(self.account.address()),
644            InstructionAccount::writable(self.mint.address()),
645            InstructionAccount::readonly_signer(self.authority.address()),
646        ];
647        let views = [self.account, self.mint, self.authority];
648        let instruction = InstructionView {
649            program_id: &TOKEN_PROGRAM_ID,
650            data: &data,
651            accounts: &accounts,
652        };
653
654        crate::cpi::invoke_signed(&instruction, &views, signers)
655    }
656}
657
658// ---------------------------------------------------------------------
659
660/// Builder for SPL Token CloseAccount (instruction index 9).
661pub struct CloseAccount<'a> {
662    pub account: &'a AccountView<'a>,
663    pub destination: &'a AccountView<'a>,
664    pub authority: &'a AccountView<'a>,
665}
666
667impl CloseAccount<'_> {
668    #[inline]
669    pub fn invoke(&self) -> ProgramResult {
670        require_authority_signed_direct(self.authority)?;
671        self.invoke_signed(&[])
672    }
673
674    #[inline]
675    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
676        self.invoke_signed_unchecked_with_multisig(&[], signers)
677    }
678
679    #[inline]
680    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
681        require_multisig_signers_direct(multisig_signers)?;
682        self.invoke_signed_multisig(multisig_signers, &[])
683    }
684
685    #[inline]
686    pub fn invoke_signed_multisig(
687        &self,
688        multisig_signers: &[&AccountView<'_>],
689        signers: &[Signer<'_, '_>],
690    ) -> ProgramResult {
691        self.invoke_signed_unchecked_with_multisig(multisig_signers, signers)
692    }
693
694    #[inline(always)]
695    fn invoke_signed_unchecked_with_multisig(
696        &self,
697        multisig_signers: &[&AccountView<'_>],
698        signers: &[Signer<'_, '_>],
699    ) -> ProgramResult {
700        let data = encoders::encode_close_account();
701        let accounts = [
702            InstructionAccount::writable(self.account.address()),
703            InstructionAccount::writable(self.destination.address()),
704            authority_meta(self.authority, multisig_signers),
705        ];
706        let views = [self.account, self.destination, self.authority];
707        invoke_token_signed(&data, accounts, views, multisig_signers, signers)
708    }
709}
710
711// ---------------------------------------------------------------------
712
713/// Builder for SPL Token Approve (instruction index 4).
714///
715/// Prefer [`ApproveChecked`] for the decimals-verified path.
716#[deprecated(
717    since = "0.2.0",
718    note = "use ApproveChecked for explicit classic SPL mint and decimals validation"
719)]
720#[cfg(feature = "legacy-token-instructions")]
721pub struct Approve<'a> {
722    pub source: &'a AccountView<'a>,
723    pub delegate: &'a AccountView<'a>,
724    pub authority: &'a AccountView<'a>,
725    pub amount: u64,
726}
727
728#[allow(deprecated)]
729#[cfg(feature = "legacy-token-instructions")]
730impl Approve<'_> {
731    #[inline]
732    pub fn invoke(&self) -> ProgramResult {
733        require_authority_signed_direct(self.authority)?;
734        self.invoke_signed(&[])
735    }
736
737    #[inline]
738    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
739        let data = encoders::encode_approve(self.amount);
740
741        let accounts = [
742            InstructionAccount::writable(self.source.address()),
743            InstructionAccount::readonly(self.delegate.address()),
744            InstructionAccount::readonly_signer(self.authority.address()),
745        ];
746        let views = [self.source, self.delegate, self.authority];
747        let instruction = InstructionView {
748            program_id: &TOKEN_PROGRAM_ID,
749            data: &data,
750            accounts: &accounts,
751        };
752
753        crate::cpi::invoke_signed(&instruction, &views, signers)
754    }
755}
756
757// ---------------------------------------------------------------------
758
759/// Builder for SPL Token Revoke (instruction index 5).
760pub struct Revoke<'a> {
761    pub source: &'a AccountView<'a>,
762    pub authority: &'a AccountView<'a>,
763}
764
765impl Revoke<'_> {
766    #[inline]
767    pub fn invoke(&self) -> ProgramResult {
768        require_authority_signed_direct(self.authority)?;
769        self.invoke_signed(&[])
770    }
771
772    #[inline]
773    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
774        self.invoke_signed_unchecked_with_multisig(&[], signers)
775    }
776
777    #[inline]
778    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
779        require_multisig_signers_direct(multisig_signers)?;
780        self.invoke_signed_multisig(multisig_signers, &[])
781    }
782
783    #[inline]
784    pub fn invoke_signed_multisig(
785        &self,
786        multisig_signers: &[&AccountView<'_>],
787        signers: &[Signer<'_, '_>],
788    ) -> ProgramResult {
789        self.invoke_signed_unchecked_with_multisig(multisig_signers, signers)
790    }
791
792    #[inline(always)]
793    fn invoke_signed_unchecked_with_multisig(
794        &self,
795        multisig_signers: &[&AccountView<'_>],
796        signers: &[Signer<'_, '_>],
797    ) -> ProgramResult {
798        let data = encoders::encode_revoke();
799        let accounts = [
800            InstructionAccount::writable(self.source.address()),
801            authority_meta(self.authority, multisig_signers),
802        ];
803        let views = [self.source, self.authority];
804        invoke_token_signed(&data, accounts, views, multisig_signers, signers)
805    }
806}
807
808// ---------------------------------------------------------------------
809//
810/// Builder for classic SPL Token TransferChecked (instruction index 12).
811///
812/// The token program checks the supplied mint and decimals. This builder calls
813/// `TOKEN_PROGRAM_ID`; it does not dispatch to Token-2022 or resolve transfer
814/// hooks. Use `hopper_solana::interface` or `hopper_token_2022` for those program
815/// integrations, supplying hook accounts when required.
816pub struct TransferChecked<'a> {
817    pub from: &'a AccountView<'a>,
818    pub mint: &'a AccountView<'a>,
819    pub to: &'a AccountView<'a>,
820    pub authority: &'a AccountView<'a>,
821    pub amount: u64,
822    pub decimals: u8,
823}
824
825impl TransferChecked<'_> {
826    /// Invoke with a transaction-signed authority. Fails fast with
827    /// `MissingRequiredSignature` before the CPI if the authority
828    /// is not a signer.
829    #[inline]
830    pub fn invoke(&self) -> ProgramResult {
831        require_authority_signed_direct(self.authority)?;
832        self.invoke_signed_unchecked(&[])
833    }
834
835    /// Check transaction signer status and require the source's token owner
836    /// to equal the authority before CPI. This stricter owner path excludes
837    /// delegated transfers; use `invoke` when the token program should validate
838    /// a delegate or `invoke_multisig` for an SPL multisig authority.
839    ///
840    /// Verifies `self.from`'s `owner` field (SPL TokenAccount bytes
841    /// `[32..64]`) matches `self.authority.address()`. Returns
842    /// `ProgramError::IncorrectAuthority` on mismatch.
843    #[inline]
844    pub fn invoke_strict(&self) -> ProgramResult {
845        require_authority_signed_direct(self.authority)?;
846        require_token_authority(self.from, self.authority)?;
847        self.invoke_signed_unchecked(&[])
848    }
849
850    /// Invoke with explicit PDA signer seeds. The SPL token program
851    /// validates mint + decimals regardless of the signer source.
852    #[inline]
853    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
854        self.invoke_signed_unchecked(signers)
855    }
856
857    /// Invoke with an SPL multisig owner account plus transaction-signed
858    /// multisig signer accounts.
859    #[inline]
860    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
861        require_multisig_signers_direct(multisig_signers)?;
862        self.invoke_signed_multisig(multisig_signers, &[])
863    }
864
865    /// Invoke with an SPL multisig owner account and explicit PDA signer seeds.
866    #[inline]
867    pub fn invoke_signed_multisig(
868        &self,
869        multisig_signers: &[&AccountView<'_>],
870        signers: &[Signer<'_, '_>],
871    ) -> ProgramResult {
872        self.invoke_signed_unchecked_with_multisig(multisig_signers, signers)
873    }
874
875    /// Strict PDA-signed invoke: ownership pre-check (the SPL token
876    /// program revalidates, but Hopper surfaces a branded error
877    /// first) then CPI with the supplied signer seeds.
878    #[inline]
879    pub fn invoke_signed_strict(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
880        require_token_authority(self.from, self.authority)?;
881        self.invoke_signed_unchecked(signers)
882    }
883
884    #[inline(always)]
885    fn invoke_signed_unchecked(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
886        self.invoke_signed_unchecked_with_multisig(&[], signers)
887    }
888
889    #[inline(always)]
890    fn invoke_signed_unchecked_with_multisig(
891        &self,
892        multisig_signers: &[&AccountView<'_>],
893        signers: &[Signer<'_, '_>],
894    ) -> ProgramResult {
895        let data = encoders::encode_transfer_checked(self.amount, self.decimals);
896
897        let accounts = [
898            InstructionAccount::writable(self.from.address()),
899            InstructionAccount::readonly(self.mint.address()),
900            InstructionAccount::writable(self.to.address()),
901            authority_meta(self.authority, multisig_signers),
902        ];
903        let views = [self.from, self.mint, self.to, self.authority];
904        invoke_token_signed(&data, accounts, views, multisig_signers, signers)
905    }
906}
907
908// ---------------------------------------------------------------------
909
910/// Builder for SPL Token MintToChecked (instruction index 14).
911///
912/// Checks decimals through the classic SPL Token program, like [`TransferChecked`].
913/// For Token-2022, use the corresponding `hopper_token_2022` builder.
914pub struct MintToChecked<'a> {
915    pub mint: &'a AccountView<'a>,
916    pub account: &'a AccountView<'a>,
917    pub mint_authority: &'a AccountView<'a>,
918    pub amount: u64,
919    pub decimals: u8,
920}
921
922impl MintToChecked<'_> {
923    #[inline]
924    pub fn invoke(&self) -> ProgramResult {
925        require_authority_signed_direct(self.mint_authority)?;
926        self.invoke_signed_unchecked(&[])
927    }
928
929    #[inline]
930    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
931        self.invoke_signed_unchecked(signers)
932    }
933
934    #[inline]
935    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
936        require_multisig_signers_direct(multisig_signers)?;
937        self.invoke_signed_multisig(multisig_signers, &[])
938    }
939
940    #[inline]
941    pub fn invoke_signed_multisig(
942        &self,
943        multisig_signers: &[&AccountView<'_>],
944        signers: &[Signer<'_, '_>],
945    ) -> ProgramResult {
946        self.invoke_signed_unchecked_with_multisig(multisig_signers, signers)
947    }
948
949    #[inline(always)]
950    fn invoke_signed_unchecked(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
951        self.invoke_signed_unchecked_with_multisig(&[], signers)
952    }
953
954    #[inline(always)]
955    fn invoke_signed_unchecked_with_multisig(
956        &self,
957        multisig_signers: &[&AccountView<'_>],
958        signers: &[Signer<'_, '_>],
959    ) -> ProgramResult {
960        let data = encoders::encode_mint_to_checked(self.amount, self.decimals);
961
962        let accounts = [
963            InstructionAccount::writable(self.mint.address()),
964            InstructionAccount::writable(self.account.address()),
965            authority_meta(self.mint_authority, multisig_signers),
966        ];
967        let views = [self.mint, self.account, self.mint_authority];
968        invoke_token_signed(&data, accounts, views, multisig_signers, signers)
969    }
970}
971
972// ---------------------------------------------------------------------
973
974/// Builder for SPL Token BurnChecked (instruction index 15).
975///
976/// Decimals-verified counterpart to the legacy `Burn` builder. Prefer this over
977/// `Burn` whenever the mint's decimals are known to the caller,
978/// so the SPL token program can reject a mis-routed call at CPI time.
979pub struct BurnChecked<'a> {
980    pub account: &'a AccountView<'a>,
981    pub mint: &'a AccountView<'a>,
982    pub authority: &'a AccountView<'a>,
983    pub amount: u64,
984    pub decimals: u8,
985}
986
987impl BurnChecked<'_> {
988    #[inline]
989    pub fn invoke(&self) -> ProgramResult {
990        require_authority_signed_direct(self.authority)?;
991        self.invoke_signed_unchecked(&[])
992    }
993
994    /// Strict invoke: signer pre-check plus token-account ownership
995    /// verification. See [`TransferChecked::invoke_strict`] for the
996    /// full rationale.
997    #[inline]
998    pub fn invoke_strict(&self) -> ProgramResult {
999        require_authority_signed_direct(self.authority)?;
1000        require_token_authority(self.account, self.authority)?;
1001        self.invoke_signed_unchecked(&[])
1002    }
1003
1004    #[inline]
1005    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1006        self.invoke_signed_unchecked(signers)
1007    }
1008
1009    #[inline]
1010    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1011        require_multisig_signers_direct(multisig_signers)?;
1012        self.invoke_signed_multisig(multisig_signers, &[])
1013    }
1014
1015    #[inline]
1016    pub fn invoke_signed_multisig(
1017        &self,
1018        multisig_signers: &[&AccountView<'_>],
1019        signers: &[Signer<'_, '_>],
1020    ) -> ProgramResult {
1021        self.invoke_signed_unchecked_with_multisig(multisig_signers, signers)
1022    }
1023
1024    /// Strict PDA-signed invoke. Pre-check the burn-source owner
1025    /// before the CPI so a misrouted signer surfaces a Hopper-branded
1026    /// error instead of an opaque SPL failure.
1027    #[inline]
1028    pub fn invoke_signed_strict(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1029        require_token_authority(self.account, self.authority)?;
1030        self.invoke_signed_unchecked(signers)
1031    }
1032
1033    #[inline(always)]
1034    fn invoke_signed_unchecked(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1035        self.invoke_signed_unchecked_with_multisig(&[], signers)
1036    }
1037
1038    #[inline(always)]
1039    fn invoke_signed_unchecked_with_multisig(
1040        &self,
1041        multisig_signers: &[&AccountView<'_>],
1042        signers: &[Signer<'_, '_>],
1043    ) -> ProgramResult {
1044        let data = encoders::encode_burn_checked(self.amount, self.decimals);
1045
1046        let accounts = [
1047            InstructionAccount::writable(self.account.address()),
1048            InstructionAccount::writable(self.mint.address()),
1049            authority_meta(self.authority, multisig_signers),
1050        ];
1051        let views = [self.account, self.mint, self.authority];
1052        invoke_token_signed(&data, accounts, views, multisig_signers, signers)
1053    }
1054}
1055
1056// ---------------------------------------------------------------------
1057
1058/// Builder for SPL Token ApproveChecked (instruction index 13).
1059///
1060/// Mint + decimals-verified approval. Same safety profile as the
1061/// other `*Checked` variants.
1062pub struct ApproveChecked<'a> {
1063    pub source: &'a AccountView<'a>,
1064    pub mint: &'a AccountView<'a>,
1065    pub delegate: &'a AccountView<'a>,
1066    pub authority: &'a AccountView<'a>,
1067    pub amount: u64,
1068    pub decimals: u8,
1069}
1070
1071impl ApproveChecked<'_> {
1072    #[inline]
1073    pub fn invoke(&self) -> ProgramResult {
1074        require_authority_signed_direct(self.authority)?;
1075        self.invoke_signed_unchecked(&[])
1076    }
1077
1078    /// Strict invoke: signer pre-check plus source-account ownership
1079    /// verification. Ensures the authority granting the approval is
1080    /// actually allowed to do so. See [`TransferChecked::invoke_strict`]
1081    /// for the full rationale.
1082    #[inline]
1083    pub fn invoke_strict(&self) -> ProgramResult {
1084        require_authority_signed_direct(self.authority)?;
1085        require_token_authority(self.source, self.authority)?;
1086        self.invoke_signed_unchecked(&[])
1087    }
1088
1089    #[inline]
1090    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1091        self.invoke_signed_unchecked(signers)
1092    }
1093
1094    #[inline]
1095    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1096        require_multisig_signers_direct(multisig_signers)?;
1097        self.invoke_signed_multisig(multisig_signers, &[])
1098    }
1099
1100    #[inline]
1101    pub fn invoke_signed_multisig(
1102        &self,
1103        multisig_signers: &[&AccountView<'_>],
1104        signers: &[Signer<'_, '_>],
1105    ) -> ProgramResult {
1106        self.invoke_signed_unchecked_with_multisig(multisig_signers, signers)
1107    }
1108
1109    /// Strict PDA-signed invoke. Pre-check the source-account owner
1110    /// before the CPI.
1111    #[inline]
1112    pub fn invoke_signed_strict(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1113        require_token_authority(self.source, self.authority)?;
1114        self.invoke_signed_unchecked(signers)
1115    }
1116
1117    #[inline(always)]
1118    fn invoke_signed_unchecked(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1119        self.invoke_signed_unchecked_with_multisig(&[], signers)
1120    }
1121
1122    #[inline(always)]
1123    fn invoke_signed_unchecked_with_multisig(
1124        &self,
1125        multisig_signers: &[&AccountView<'_>],
1126        signers: &[Signer<'_, '_>],
1127    ) -> ProgramResult {
1128        let data = encoders::encode_approve_checked(self.amount, self.decimals);
1129
1130        let accounts = [
1131            InstructionAccount::writable(self.source.address()),
1132            InstructionAccount::readonly(self.mint.address()),
1133            InstructionAccount::readonly(self.delegate.address()),
1134            authority_meta(self.authority, multisig_signers),
1135        ];
1136        let views = [self.source, self.mint, self.delegate, self.authority];
1137        invoke_token_signed(&data, accounts, views, multisig_signers, signers)
1138    }
1139}
1140
1141// ---------------------------------------------------------------------
1142
1143/// Authority classes accepted by SPL Token's SetAuthority instruction.
1144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1145#[repr(u8)]
1146pub enum TokenAuthorityType {
1147    MintTokens = 0,
1148    FreezeAccount = 1,
1149    AccountOwner = 2,
1150    CloseAccount = 3,
1151}
1152
1153/// Builder for SPL Token SetAuthority (instruction index 6).
1154pub struct SetAuthority<'a> {
1155    pub account: &'a AccountView<'a>,
1156    pub current_authority: &'a AccountView<'a>,
1157    pub authority_type: TokenAuthorityType,
1158    pub new_authority: Option<&'a Address>,
1159}
1160
1161impl SetAuthority<'_> {
1162    #[inline]
1163    pub fn invoke(&self) -> ProgramResult {
1164        require_authority_signed_direct(self.current_authority)?;
1165        self.invoke_signed_unchecked_with_multisig(&[], &[])
1166    }
1167
1168    #[inline]
1169    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1170        self.invoke_signed_unchecked_with_multisig(&[], signers)
1171    }
1172
1173    #[inline]
1174    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1175        require_multisig_signers_direct(multisig_signers)?;
1176        self.invoke_signed_multisig(multisig_signers, &[])
1177    }
1178
1179    #[inline]
1180    pub fn invoke_signed_multisig(
1181        &self,
1182        multisig_signers: &[&AccountView<'_>],
1183        signers: &[Signer<'_, '_>],
1184    ) -> ProgramResult {
1185        self.invoke_signed_unchecked_with_multisig(multisig_signers, signers)
1186    }
1187
1188    #[inline(always)]
1189    fn invoke_signed_unchecked_with_multisig(
1190        &self,
1191        multisig_signers: &[&AccountView<'_>],
1192        signers: &[Signer<'_, '_>],
1193    ) -> ProgramResult {
1194        let (data, len) = encoders::encode_set_authority(
1195            self.authority_type as u8,
1196            self.new_authority.map(|a| a.as_array()),
1197        );
1198        let accounts = [
1199            InstructionAccount::writable(self.account.address()),
1200            authority_meta(self.current_authority, multisig_signers),
1201        ];
1202        let views = [self.account, self.current_authority];
1203        invoke_token_signed(&data[..len], accounts, views, multisig_signers, signers)
1204    }
1205}
1206
1207// ---------------------------------------------------------------------
1208
1209/// Builder for SPL Token FreezeAccount (instruction index 10).
1210pub struct FreezeAccount<'a> {
1211    pub account: &'a AccountView<'a>,
1212    pub mint: &'a AccountView<'a>,
1213    pub freeze_authority: &'a AccountView<'a>,
1214}
1215
1216impl FreezeAccount<'_> {
1217    #[inline]
1218    pub fn invoke(&self) -> ProgramResult {
1219        require_authority_signed_direct(self.freeze_authority)?;
1220        self.invoke_signed_unchecked_with_multisig(&[], &[])
1221    }
1222
1223    #[inline]
1224    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1225        self.invoke_signed_unchecked_with_multisig(&[], signers)
1226    }
1227
1228    #[inline]
1229    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1230        require_multisig_signers_direct(multisig_signers)?;
1231        self.invoke_signed_multisig(multisig_signers, &[])
1232    }
1233
1234    #[inline]
1235    pub fn invoke_signed_multisig(
1236        &self,
1237        multisig_signers: &[&AccountView<'_>],
1238        signers: &[Signer<'_, '_>],
1239    ) -> ProgramResult {
1240        self.invoke_signed_unchecked_with_multisig(multisig_signers, signers)
1241    }
1242
1243    #[inline(always)]
1244    fn invoke_signed_unchecked_with_multisig(
1245        &self,
1246        multisig_signers: &[&AccountView<'_>],
1247        signers: &[Signer<'_, '_>],
1248    ) -> ProgramResult {
1249        let data = encoders::encode_freeze_account();
1250        let accounts = [
1251            InstructionAccount::writable(self.account.address()),
1252            InstructionAccount::readonly(self.mint.address()),
1253            authority_meta(self.freeze_authority, multisig_signers),
1254        ];
1255        let views = [self.account, self.mint, self.freeze_authority];
1256        invoke_token_signed(&data, accounts, views, multisig_signers, signers)
1257    }
1258}
1259
1260/// Builder for SPL Token ThawAccount (instruction index 11).
1261pub struct ThawAccount<'a> {
1262    pub account: &'a AccountView<'a>,
1263    pub mint: &'a AccountView<'a>,
1264    pub freeze_authority: &'a AccountView<'a>,
1265}
1266
1267impl ThawAccount<'_> {
1268    #[inline]
1269    pub fn invoke(&self) -> ProgramResult {
1270        require_authority_signed_direct(self.freeze_authority)?;
1271        self.invoke_signed_unchecked_with_multisig(&[], &[])
1272    }
1273
1274    #[inline]
1275    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
1276        self.invoke_signed_unchecked_with_multisig(&[], signers)
1277    }
1278
1279    #[inline]
1280    pub fn invoke_multisig(&self, multisig_signers: &[&AccountView<'_>]) -> ProgramResult {
1281        require_multisig_signers_direct(multisig_signers)?;
1282        self.invoke_signed_multisig(multisig_signers, &[])
1283    }
1284
1285    #[inline]
1286    pub fn invoke_signed_multisig(
1287        &self,
1288        multisig_signers: &[&AccountView<'_>],
1289        signers: &[Signer<'_, '_>],
1290    ) -> ProgramResult {
1291        self.invoke_signed_unchecked_with_multisig(multisig_signers, signers)
1292    }
1293
1294    #[inline(always)]
1295    fn invoke_signed_unchecked_with_multisig(
1296        &self,
1297        multisig_signers: &[&AccountView<'_>],
1298        signers: &[Signer<'_, '_>],
1299    ) -> ProgramResult {
1300        let data = encoders::encode_thaw_account();
1301        let accounts = [
1302            InstructionAccount::writable(self.account.address()),
1303            InstructionAccount::readonly(self.mint.address()),
1304            authority_meta(self.freeze_authority, multisig_signers),
1305        ];
1306        let views = [self.account, self.mint, self.freeze_authority];
1307        invoke_token_signed(&data, accounts, views, multisig_signers, signers)
1308    }
1309}
1310
1311// ---------------------------------------------------------------------
1312
1313/// Builder for SPL Token SyncNative (instruction index 17).
1314pub struct SyncNative<'a> {
1315    pub account: &'a AccountView<'a>,
1316}
1317
1318impl SyncNative<'_> {
1319    #[inline]
1320    pub fn invoke(&self) -> ProgramResult {
1321        let data = encoders::encode_sync_native();
1322        let accounts = [InstructionAccount::writable(self.account.address())];
1323        let views = [self.account];
1324        invoke_token_signed(&data, accounts, views, &[], &[])
1325    }
1326}
1327
1328// ---------------------------------------------------------------------
1329
1330/// Builder for SPL Token InitializeAccount (instruction index 1).
1331pub struct InitializeAccount<'a> {
1332    pub account: &'a AccountView<'a>,
1333    pub mint: &'a AccountView<'a>,
1334    pub owner: &'a AccountView<'a>,
1335    pub rent_sysvar: &'a AccountView<'a>,
1336}
1337
1338impl InitializeAccount<'_> {
1339    #[inline]
1340    pub fn invoke(&self) -> ProgramResult {
1341        let data = encoders::encode_initialize_account();
1342        let accounts = [
1343            InstructionAccount::writable(self.account.address()),
1344            InstructionAccount::readonly(self.mint.address()),
1345            InstructionAccount::readonly(self.owner.address()),
1346            InstructionAccount::readonly(self.rent_sysvar.address()),
1347        ];
1348        let views = [self.account, self.mint, self.owner, self.rent_sysvar];
1349        let instruction = InstructionView {
1350            program_id: &TOKEN_PROGRAM_ID,
1351            data: &data,
1352            accounts: &accounts,
1353        };
1354
1355        crate::cpi::invoke(&instruction, &views)
1356    }
1357}
1358
1359/// Builder for SPL Token InitializeAccount2 (instruction index 16).
1360pub struct InitializeAccount2<'a> {
1361    pub account: &'a AccountView<'a>,
1362    pub mint: &'a AccountView<'a>,
1363    pub owner: &'a Address,
1364    pub rent_sysvar: &'a AccountView<'a>,
1365}
1366
1367impl InitializeAccount2<'_> {
1368    #[inline]
1369    pub fn invoke(&self) -> ProgramResult {
1370        let data = encoders::encode_initialize_account_with_owner(16, self.owner.as_array());
1371        let accounts = [
1372            InstructionAccount::writable(self.account.address()),
1373            InstructionAccount::readonly(self.mint.address()),
1374            InstructionAccount::readonly(self.rent_sysvar.address()),
1375        ];
1376        let views = [self.account, self.mint, self.rent_sysvar];
1377        invoke_token_signed(&data, accounts, views, &[], &[])
1378    }
1379}
1380
1381/// Builder for SPL Token InitializeAccount3 (instruction index 18).
1382pub struct InitializeAccount3<'a> {
1383    pub account: &'a AccountView<'a>,
1384    pub mint: &'a AccountView<'a>,
1385    pub owner: &'a Address,
1386}
1387
1388impl InitializeAccount3<'_> {
1389    #[inline]
1390    pub fn invoke(&self) -> ProgramResult {
1391        let data = encoders::encode_initialize_account_with_owner(18, self.owner.as_array());
1392        let accounts = [
1393            InstructionAccount::writable(self.account.address()),
1394            InstructionAccount::readonly(self.mint.address()),
1395        ];
1396        let views = [self.account, self.mint];
1397        invoke_token_signed(&data, accounts, views, &[], &[])
1398    }
1399}
1400
1401/// SPL Token program address.
1402pub const TOKEN_PROGRAM_ID: Address = Address::new_from_array(crate::__decode_base58_32(
1403    "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
1404));
1405
1406// ---------------------------------------------------------------------
1407
1408pub const SPL_TOKEN_ACCOUNT_LEN: usize = 165;
1409pub const SPL_MINT_LEN: usize = 82;
1410
1411const TOKEN_ACCOUNT_MINT_OFFSET: usize = 0;
1412const TOKEN_ACCOUNT_AUTHORITY_OFFSET: usize = 32;
1413const TOKEN_ACCOUNT_AMOUNT_OFFSET: usize = 64;
1414const TOKEN_ACCOUNT_STATE_OFFSET: usize = 108;
1415
1416const MINT_AUTHORITY_TAG_OFFSET: usize = 0;
1417const MINT_AUTHORITY_OFFSET: usize = 4;
1418const MINT_SUPPLY_OFFSET: usize = 36;
1419const MINT_DECIMALS_OFFSET: usize = 44;
1420const MINT_INITIALIZED_OFFSET: usize = 45;
1421const MINT_FREEZE_AUTHORITY_TAG_OFFSET: usize = 46;
1422const MINT_FREEZE_AUTHORITY_OFFSET: usize = 50;
1423
1424/// Known external SPL TokenAccount adapter.
1425pub struct SplTokenAccount;
1426
1427/// Guard-owned zero-copy SPL TokenAccount view.
1428pub struct SplTokenAccountView<'a> {
1429    data: Ref<'a, [u8]>,
1430}
1431
1432impl SplTokenAccountView<'_> {
1433    #[inline(always)]
1434    pub fn mint(&self) -> Address {
1435        read_address_unchecked(&self.data, TOKEN_ACCOUNT_MINT_OFFSET)
1436    }
1437
1438    #[inline(always)]
1439    pub fn authority(&self) -> Address {
1440        read_address_unchecked(&self.data, TOKEN_ACCOUNT_AUTHORITY_OFFSET)
1441    }
1442
1443    #[inline(always)]
1444    pub fn amount(&self) -> u64 {
1445        read_u64_unchecked(&self.data, TOKEN_ACCOUNT_AMOUNT_OFFSET)
1446    }
1447
1448    #[inline(always)]
1449    pub fn state(&self) -> u8 {
1450        self.data[TOKEN_ACCOUNT_STATE_OFFSET]
1451    }
1452
1453    #[inline(always)]
1454    pub fn is_initialized(&self) -> bool {
1455        self.state() != 0
1456    }
1457}
1458
1459impl ExternalZeroCopy for SplTokenAccount {
1460    type View<'a> = SplTokenAccountView<'a>;
1461
1462    const OWNER: Option<Address> = Some(TOKEN_PROGRAM_ID);
1463    const MIN_LEN: usize = SPL_TOKEN_ACCOUNT_LEN;
1464
1465    #[inline]
1466    fn view<'a>(data: Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError> {
1467        Ok(SplTokenAccountView { data })
1468    }
1469}
1470
1471impl ExplainExternal for SplTokenAccount {
1472    fn explain<S: ExternalExplainSink>(account: &AccountView<'_>, sink: &mut S) -> ProgramResult {
1473        let account = ExternalAccount::<SplTokenAccount>::try_new(account)?;
1474        account.with_view(|token| {
1475            sink.field_str("adapter", "SplTokenAccount")?;
1476            sink.field_address("mint", &token.mint())?;
1477            sink.field_address("authority", &token.authority())?;
1478            sink.field_u64("amount", token.amount())?;
1479            sink.field_bool("initialized", token.is_initialized())
1480        })
1481    }
1482}
1483
1484/// Known external SPL Mint adapter.
1485pub struct SplMint;
1486
1487/// Guard-owned zero-copy SPL Mint view.
1488pub struct SplMintView<'a> {
1489    data: Ref<'a, [u8]>,
1490}
1491
1492impl SplMintView<'_> {
1493    #[inline(always)]
1494    pub fn mint_authority(&self) -> Option<Address> {
1495        read_coption_address(&self.data, MINT_AUTHORITY_TAG_OFFSET, MINT_AUTHORITY_OFFSET)
1496    }
1497
1498    #[inline(always)]
1499    pub fn supply(&self) -> u64 {
1500        read_u64_unchecked(&self.data, MINT_SUPPLY_OFFSET)
1501    }
1502
1503    #[inline(always)]
1504    pub fn decimals(&self) -> u8 {
1505        self.data[MINT_DECIMALS_OFFSET]
1506    }
1507
1508    #[inline(always)]
1509    pub fn is_initialized(&self) -> bool {
1510        self.data[MINT_INITIALIZED_OFFSET] != 0
1511    }
1512
1513    #[inline(always)]
1514    pub fn freeze_authority(&self) -> Option<Address> {
1515        read_coption_address(
1516            &self.data,
1517            MINT_FREEZE_AUTHORITY_TAG_OFFSET,
1518            MINT_FREEZE_AUTHORITY_OFFSET,
1519        )
1520    }
1521}
1522
1523impl ExternalZeroCopy for SplMint {
1524    type View<'a> = SplMintView<'a>;
1525
1526    const OWNER: Option<Address> = Some(TOKEN_PROGRAM_ID);
1527    const MIN_LEN: usize = SPL_MINT_LEN;
1528
1529    #[inline]
1530    fn view<'a>(data: Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError> {
1531        Ok(SplMintView { data })
1532    }
1533}
1534
1535impl ExplainExternal for SplMint {
1536    fn explain<S: ExternalExplainSink>(account: &AccountView<'_>, sink: &mut S) -> ProgramResult {
1537        let account = ExternalAccount::<SplMint>::try_new(account)?;
1538        account.with_view(|mint| {
1539            sink.field_str("adapter", "SplMint")?;
1540            sink.field_u64("supply", mint.supply())?;
1541            sink.field_u64("decimals", mint.decimals() as u64)?;
1542            sink.field_bool("initialized", mint.is_initialized())
1543        })
1544    }
1545}
1546
1547/// Proof token that an SPL TokenAccount matched an expected mint.
1548#[derive(Debug)]
1549pub struct CheckedTokenMint<'info> {
1550    account: ExternalAccount<'info, SplTokenAccount>,
1551    mint: Address,
1552}
1553
1554impl<'info> CheckedTokenMint<'info> {
1555    #[inline(always)]
1556    pub const fn account(&self) -> ExternalAccount<'info, SplTokenAccount> {
1557        self.account
1558    }
1559
1560    #[inline(always)]
1561    pub const fn mint(&self) -> Address {
1562        self.mint
1563    }
1564}
1565
1566/// Proof token that an SPL TokenAccount matched an expected token authority.
1567#[derive(Debug)]
1568pub struct CheckedTokenAuthority<'info> {
1569    account: ExternalAccount<'info, SplTokenAccount>,
1570    authority: Address,
1571}
1572
1573impl<'info> CheckedTokenAuthority<'info> {
1574    #[inline(always)]
1575    pub const fn account(&self) -> ExternalAccount<'info, SplTokenAccount> {
1576        self.account
1577    }
1578
1579    #[inline(always)]
1580    pub const fn authority(&self) -> Address {
1581        self.authority
1582    }
1583}
1584
1585/// Proof token that an SPL Mint matched expected decimals.
1586#[derive(Debug)]
1587pub struct CheckedMintDecimals<'info> {
1588    account: ExternalAccount<'info, SplMint>,
1589    decimals: u8,
1590}
1591
1592impl<'info> CheckedMintDecimals<'info> {
1593    #[inline(always)]
1594    pub const fn account(&self) -> ExternalAccount<'info, SplMint> {
1595        self.account
1596    }
1597
1598    #[inline(always)]
1599    pub const fn decimals(&self) -> u8 {
1600        self.decimals
1601    }
1602}
1603
1604/// Snapshot of a token account amount before CPI.
1605#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1606pub struct TokenAmountSnapshot {
1607    amount: u64,
1608}
1609
1610impl TokenAmountSnapshot {
1611    #[inline(always)]
1612    pub const fn amount(self) -> u64 {
1613        self.amount
1614    }
1615}
1616
1617impl<'info> ExternalAccount<'info, SplTokenAccount> {
1618    #[inline]
1619    pub fn token_amount(&self) -> Result<u64, ProgramError> {
1620        Ok(self.view()?.amount())
1621    }
1622
1623    #[inline]
1624    pub fn checked_mint(
1625        &self,
1626        expected_mint: &Address,
1627    ) -> Result<CheckedTokenMint<'info>, ProgramError> {
1628        let mint = self.view()?.mint();
1629        if &mint == expected_mint {
1630            Ok(CheckedTokenMint {
1631                account: *self,
1632                mint,
1633            })
1634        } else {
1635            Err(ProgramError::InvalidAccountData)
1636        }
1637    }
1638
1639    #[inline]
1640    pub fn checked_authority(
1641        &self,
1642        expected_authority: &Address,
1643    ) -> Result<CheckedTokenAuthority<'info>, ProgramError> {
1644        let authority = self.view()?.authority();
1645        if &authority == expected_authority {
1646            Ok(CheckedTokenAuthority {
1647                account: *self,
1648                authority,
1649            })
1650        } else {
1651            Err(ProgramError::IncorrectAuthority)
1652        }
1653    }
1654
1655    #[inline]
1656    pub fn amount_snapshot(&self) -> Result<TokenAmountSnapshot, ProgramError> {
1657        Ok(TokenAmountSnapshot {
1658            amount: self.token_amount()?,
1659        })
1660    }
1661
1662    #[inline]
1663    pub fn assert_amount_delta(
1664        &self,
1665        before: TokenAmountSnapshot,
1666        expected_delta: i128,
1667    ) -> ProgramResult {
1668        let after = self.token_amount()? as i128;
1669        let expected = (before.amount as i128)
1670            .checked_add(expected_delta)
1671            .ok_or(ProgramError::ArithmeticOverflow)?;
1672        if expected < 0 || expected > u64::MAX as i128 {
1673            return Err(ProgramError::ArithmeticOverflow);
1674        }
1675        if after == expected {
1676            Ok(())
1677        } else {
1678            Err(ProgramError::InvalidAccountData)
1679        }
1680    }
1681
1682    #[inline]
1683    pub fn assert_amount_unchanged(&self, before: TokenAmountSnapshot) -> ProgramResult {
1684        self.assert_amount_delta(before, 0)
1685    }
1686}
1687
1688impl<'info> ExternalAccount<'info, SplMint> {
1689    #[inline]
1690    pub fn checked_decimals(
1691        &self,
1692        expected: u8,
1693    ) -> Result<CheckedMintDecimals<'info>, ProgramError> {
1694        let decimals = self.view()?.decimals();
1695        if decimals == expected {
1696            Ok(CheckedMintDecimals {
1697                account: *self,
1698                decimals,
1699            })
1700        } else {
1701            Err(ProgramError::InvalidAccountData)
1702        }
1703    }
1704}
1705
1706#[inline(always)]
1707fn read_address_unchecked(data: &[u8], offset: usize) -> Address {
1708    let mut bytes = [0u8; 32];
1709    bytes.copy_from_slice(&data[offset..offset + 32]);
1710    Address::new_from_array(bytes)
1711}
1712
1713#[inline(always)]
1714fn read_u64_unchecked(data: &[u8], offset: usize) -> u64 {
1715    u64::from_le_bytes([
1716        data[offset],
1717        data[offset + 1],
1718        data[offset + 2],
1719        data[offset + 3],
1720        data[offset + 4],
1721        data[offset + 5],
1722        data[offset + 6],
1723        data[offset + 7],
1724    ])
1725}
1726
1727#[inline(always)]
1728fn read_u32_unchecked(data: &[u8], offset: usize) -> u32 {
1729    u32::from_le_bytes([
1730        data[offset],
1731        data[offset + 1],
1732        data[offset + 2],
1733        data[offset + 3],
1734    ])
1735}
1736
1737#[inline(always)]
1738fn read_coption_address(data: &[u8], tag_offset: usize, address_offset: usize) -> Option<Address> {
1739    match read_u32_unchecked(data, tag_offset) {
1740        1 => Some(read_address_unchecked(data, address_offset)),
1741        _ => None,
1742    }
1743}
1744
1745/// Legacy module-path re-exports.
1746pub mod instructions {
1747    pub use super::{
1748        ApproveChecked, BurnChecked, CloseAccount, FreezeAccount, InitializeAccount,
1749        InitializeAccount2, InitializeAccount3, MintToChecked, Revoke, SetAuthority, SyncNative,
1750        ThawAccount, TokenAuthorityType, TransferChecked,
1751    };
1752
1753    #[cfg(feature = "legacy-token-instructions")]
1754    #[allow(deprecated)]
1755    pub use super::{Approve, Burn, MintTo, Transfer};
1756}
1757
1758#[cfg(test)]
1759mod tests {
1760    //! Wire-format regression tests for the builder instruction-data.
1761    //!
1762    //! The SPL token program decodes every instruction by its first
1763    //! byte, so getting the discriminator wrong silently routes to
1764    //! a different op. These tests lock the exact byte layout each
1765    //! builder produces.
1766
1767    use super::*;
1768    use hopper_native::{
1769        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
1770    };
1771    fn make_account(owner: Address, data: &[u8]) -> (std::vec::Vec<u64>, AccountView<'static>) {
1772        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + data.len()).div_ceil(8)];
1773        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
1774        // SAFETY: Test helper writes a valid RuntimeAccount header and copies
1775        // payload bytes into owned backing memory.
1776        unsafe {
1777            raw.write(RuntimeAccount {
1778                borrow_state: NOT_BORROWED,
1779                is_signer: 0,
1780                is_writable: 1,
1781                executable: 0,
1782                resize_delta: 0,
1783                address: NativeAddress::new_from_array([7; 32]),
1784                owner: NativeAddress::new_from_array(owner.to_bytes()),
1785                lamports: 1,
1786                data_len: data.len() as u64,
1787            });
1788            let data_ptr = (backing.as_mut_ptr() as *mut u8).add(RuntimeAccount::SIZE);
1789            core::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, data.len());
1790        }
1791        // SAFETY: `raw` points at the initialized RuntimeAccount header.
1792        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
1793        (backing, AccountView::from_backend(backend))
1794    }
1795    fn token_account_data(
1796        mint: Address,
1797        authority: Address,
1798        amount: u64,
1799    ) -> [u8; SPL_TOKEN_ACCOUNT_LEN] {
1800        let mut data = [0u8; SPL_TOKEN_ACCOUNT_LEN];
1801        data[0..32].copy_from_slice(mint.as_bytes());
1802        data[32..64].copy_from_slice(authority.as_bytes());
1803        data[64..72].copy_from_slice(&amount.to_le_bytes());
1804        data[108] = 1;
1805        data
1806    }
1807    fn mint_data(authority: Address, supply: u64, decimals: u8) -> [u8; SPL_MINT_LEN] {
1808        let mut data = [0u8; SPL_MINT_LEN];
1809        data[0..4].copy_from_slice(&1u32.to_le_bytes());
1810        data[4..36].copy_from_slice(authority.as_bytes());
1811        data[36..44].copy_from_slice(&supply.to_le_bytes());
1812        data[44] = decimals;
1813        data[45] = 1;
1814        data
1815    }
1816
1817    // Verify the discriminator byte of each `*Checked` variant
1818    // matches the SPL Token program's public definition. These are
1819    // stability tests: if SPL ever renumbered indices the builder
1820    // would silently route to the wrong instruction without them.
1821    #[test]
1822    fn transfer_checked_discriminator_is_12() {
1823        // The SPL Token program's instruction enum assigns:
1824        //   0 = InitializeMint
1825        //   3 = Transfer
1826        //  12 = TransferChecked
1827        //  13 = ApproveChecked
1828        //  14 = MintToChecked
1829        //  15 = BurnChecked
1830        // We assert each builder hard-codes the right index.
1831        //
1832        // We can't instantiate a builder without an `AccountView`,
1833        // but we can read the constant directly from the source by
1834        // looking at the first byte the `invoke_signed_unchecked`
1835        // writes. Expressing that here as a documentation-level
1836        // contract, the wire-format tests below build a real data
1837        // buffer and lock the discriminator there.
1838        //
1839        // Keep these tests if the SPL Token program adds new
1840        // instructions that might conflict; they pin our build to
1841        // the canonical numbering.
1842    }
1843    #[test]
1844    fn spl_external_token_account_view_proofs_and_amount_delta() {
1845        let mint = Address::new_from_array([2; 32]);
1846        let authority = Address::new_from_array([3; 32]);
1847        let data = token_account_data(mint, authority, 100);
1848        let (mut backing, account) = make_account(TOKEN_PROGRAM_ID, &data);
1849
1850        let token = ExternalAccount::<SplTokenAccount>::try_new(&account).unwrap();
1851        let view = token.view().unwrap();
1852        assert_eq!(view.mint(), mint);
1853        assert_eq!(view.authority(), authority);
1854        assert_eq!(view.amount(), 100);
1855        assert!(view.is_initialized());
1856        assert_eq!(token.checked_mint(&mint).unwrap().mint(), mint);
1857        assert_eq!(
1858            token.checked_authority(&authority).unwrap().authority(),
1859            authority
1860        );
1861        assert_eq!(
1862            token
1863                .checked_mint(&Address::new_from_array([9; 32]))
1864                .unwrap_err(),
1865            ProgramError::InvalidAccountData
1866        );
1867
1868        let before = token.amount_snapshot().unwrap();
1869        // Byte view over the word-aligned backing (the fixture keeps the
1870        // allocation 8-aligned for the RuntimeAccount header).
1871        // SAFETY: `backing` owns these bytes; u8 has no alignment demands.
1872        let backing_bytes = unsafe {
1873            core::slice::from_raw_parts_mut(backing.as_mut_ptr() as *mut u8, backing.len() * 8)
1874        };
1875        backing_bytes[RuntimeAccount::SIZE + 64..RuntimeAccount::SIZE + 72]
1876            .copy_from_slice(&150u64.to_le_bytes());
1877        token.assert_amount_delta(before, 50).unwrap();
1878        assert_eq!(
1879            token.assert_amount_delta(before, 49).unwrap_err(),
1880            ProgramError::InvalidAccountData
1881        );
1882    }
1883    #[test]
1884    fn spl_external_mint_view_and_decimals_proof() {
1885        let authority = Address::new_from_array([4; 32]);
1886        let data = mint_data(authority, 1_000_000, 6);
1887        let (_backing, account) = make_account(TOKEN_PROGRAM_ID, &data);
1888
1889        let mint = ExternalAccount::<SplMint>::try_new(&account).unwrap();
1890        let view = mint.view().unwrap();
1891        assert_eq!(view.mint_authority(), Some(authority));
1892        assert_eq!(view.supply(), 1_000_000);
1893        assert_eq!(view.decimals(), 6);
1894        assert!(view.is_initialized());
1895        assert_eq!(mint.checked_decimals(6).unwrap().decimals(), 6);
1896        assert_eq!(
1897            mint.checked_decimals(9).unwrap_err(),
1898            ProgramError::InvalidAccountData
1899        );
1900    }
1901
1902    /// Helper: reconstruct the 10-byte instruction-data buffer a
1903    /// `*Checked` builder writes, bypassing the CPI so the test has
1904    /// no AccountView dependency.
1905    fn encode_checked(disc: u8, amount: u64, decimals: u8) -> [u8; 10] {
1906        let mut data = [0u8; 10];
1907        data[0] = disc;
1908        data[1..9].copy_from_slice(&amount.to_le_bytes());
1909        data[9] = decimals;
1910        data
1911    }
1912
1913    #[test]
1914    fn transfer_checked_wire_format_is_stable() {
1915        // 12, amount LE, decimals = [12, a0..a7, dec]
1916        let out = encode_checked(12, 0x0102_0304_0506_0708, 9);
1917        assert_eq!(out[0], 12);
1918        assert_eq!(
1919            &out[1..9],
1920            &[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]
1921        );
1922        assert_eq!(out[9], 9);
1923    }
1924
1925    #[test]
1926    fn mint_to_checked_wire_format_is_stable() {
1927        let out = encode_checked(14, 1000, 6);
1928        assert_eq!(out[0], 14);
1929        assert_eq!(u64::from_le_bytes(out[1..9].try_into().unwrap()), 1000);
1930        assert_eq!(out[9], 6);
1931    }
1932
1933    #[test]
1934    fn burn_checked_wire_format_is_stable() {
1935        let out = encode_checked(15, 42, 8);
1936        assert_eq!(out[0], 15);
1937        assert_eq!(u64::from_le_bytes(out[1..9].try_into().unwrap()), 42);
1938        assert_eq!(out[9], 8);
1939    }
1940
1941    #[test]
1942    fn approve_checked_wire_format_is_stable() {
1943        let out = encode_checked(13, u64::MAX, 0);
1944        assert_eq!(out[0], 13);
1945        assert_eq!(u64::from_le_bytes(out[1..9].try_into().unwrap()), u64::MAX);
1946        assert_eq!(out[9], 0);
1947    }
1948
1949    #[test]
1950    fn checked_encoding_round_trips_decimals_range() {
1951        // 0..=255 decimals must all survive the encode. Some SPL
1952        // mints have decimals > 9 (e.g. native SOL = 9; synthetic
1953        // mints use larger values).
1954        for d in 0u8..=255 {
1955            let out = encode_checked(12, 1, d);
1956            assert_eq!(out[9], d);
1957        }
1958    }
1959
1960    #[test]
1961    fn checked_encoding_preserves_amount_bits() {
1962        // Every byte in the amount field must land at its expected
1963        // little-endian slot.
1964        for shift in 0..8 {
1965            let amount = 0xABu64 << (shift * 8);
1966            let out = encode_checked(12, amount, 0);
1967            let decoded = u64::from_le_bytes(out[1..9].try_into().unwrap());
1968            assert_eq!(decoded, amount);
1969        }
1970    }
1971
1972    #[test]
1973    fn authority_and_initialize_encodings_match_spl_token_wire_format() {
1974        let authority = Address::new_from_array([9; 32]);
1975        let (set_authority, len) = encoders::encode_set_authority(
1976            TokenAuthorityType::AccountOwner as u8,
1977            Some(authority.as_array()),
1978        );
1979        assert_eq!(len, 35);
1980        assert_eq!(set_authority[0], 6);
1981        assert_eq!(set_authority[1], 2);
1982        assert_eq!(set_authority[2], 1);
1983        assert_eq!(&set_authority[3..35], authority.as_bytes());
1984
1985        let (set_authority, len) =
1986            encoders::encode_set_authority(TokenAuthorityType::CloseAccount as u8, None);
1987        assert_eq!(len, 3);
1988        assert_eq!(&set_authority[..3], &[6, 3, 0]);
1989
1990        let init2 = encoders::encode_initialize_account_with_owner(16, authority.as_array());
1991        let init3 = encoders::encode_initialize_account_with_owner(18, authority.as_array());
1992        assert_eq!(init2[0], 16);
1993        assert_eq!(init3[0], 18);
1994        assert_eq!(&init2[1..33], authority.as_bytes());
1995        assert_eq!(&init3[1..33], authority.as_bytes());
1996    }
1997
1998    /// Byte-identity guard for the extracted [`encoders`] module: each
1999    /// shipped encoder must reproduce the exact bytes the builders wrote
2000    /// inline before the refactor. These literals are the pre-refactor wire
2001    /// bytes; if any diverges, a CPI's instruction-data changed.
2002    #[test]
2003    fn shipped_encoders_match_pre_refactor_golden_bytes() {
2004        // amount = 1 → little-endian 01 00 00 00 00 00 00 00.
2005        assert_eq!(encoders::encode_transfer(1), [3, 1, 0, 0, 0, 0, 0, 0, 0]);
2006        assert_eq!(encoders::encode_approve(1), [4, 1, 0, 0, 0, 0, 0, 0, 0]);
2007        assert_eq!(encoders::encode_mint_to(1), [7, 1, 0, 0, 0, 0, 0, 0, 0]);
2008        assert_eq!(encoders::encode_burn(1), [8, 1, 0, 0, 0, 0, 0, 0, 0]);
2009        assert_eq!(
2010            encoders::encode_transfer_checked(1, 9),
2011            [12, 1, 0, 0, 0, 0, 0, 0, 0, 9]
2012        );
2013        assert_eq!(
2014            encoders::encode_approve_checked(1, 9),
2015            [13, 1, 0, 0, 0, 0, 0, 0, 0, 9]
2016        );
2017        assert_eq!(
2018            encoders::encode_mint_to_checked(1, 9),
2019            [14, 1, 0, 0, 0, 0, 0, 0, 0, 9]
2020        );
2021        assert_eq!(
2022            encoders::encode_burn_checked(1, 9),
2023            [15, 1, 0, 0, 0, 0, 0, 0, 0, 9]
2024        );
2025        assert_eq!(encoders::encode_revoke(), [5]);
2026        assert_eq!(encoders::encode_close_account(), [9]);
2027        assert_eq!(encoders::encode_freeze_account(), [10]);
2028        assert_eq!(encoders::encode_thaw_account(), [11]);
2029        assert_eq!(encoders::encode_sync_native(), [17]);
2030        assert_eq!(encoders::encode_initialize_account(), [1]);
2031
2032        let owner = [
2033            0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
2034            24, 25, 26, 27, 28, 29, 30, 31,
2035        ];
2036        let init2 = encoders::encode_initialize_account_with_owner(16, &owner);
2037        assert_eq!(init2[0], 16);
2038        assert_eq!(&init2[1..33], &owner);
2039        let init3 = encoders::encode_initialize_account_with_owner(18, &owner);
2040        assert_eq!(init3[0], 18);
2041        assert_eq!(&init3[1..33], &owner);
2042
2043        let (sa_some, some_len) = encoders::encode_set_authority(2, Some(&owner));
2044        assert_eq!(some_len, 35);
2045        assert_eq!(sa_some[0], 6);
2046        assert_eq!(sa_some[1], 2);
2047        assert_eq!(sa_some[2], 1);
2048        assert_eq!(&sa_some[3..35], &owner);
2049        let (sa_none, none_len) = encoders::encode_set_authority(3, None);
2050        assert_eq!(none_len, 3);
2051        assert_eq!(&sa_none[..3], &[6, 3, 0]);
2052    }
2053
2054    // ---------------------------------------------------------------------
2055
2056    /// Build a minimal valid SPL TokenAccount data buffer + an
2057    /// AccountView wrapping it, plus a matching authority view. The
2058    /// token account's `owner` field (bytes [32..64]) is set to the
2059    /// requested authority so the ownership check passes by default;
2060    /// individual tests can mutate the buffer to exercise mismatch.
2061    fn make_token_and_authority(
2062        authority_bytes: [u8; 32],
2063        token_owner_bytes: [u8; 32],
2064    ) -> (
2065        std::vec::Vec<u64>,
2066        std::vec::Vec<u64>,
2067        crate::account::AccountView<'static>,
2068        crate::account::AccountView<'static>,
2069    ) {
2070        use hopper_native::{
2071            AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
2072            NOT_BORROWED,
2073        };
2074
2075        // TokenAccount: SPL layout is 165 bytes; first 32 bytes are
2076        // `mint`, next 32 are `owner`. We only care about the owner
2077        // slot for `require_token_authority`, but size the buffer at
2078        // 165 so it looks like a real TokenAccount.
2079        let token_data_len = 165;
2080        let mut token_backing =
2081            std::vec![0u64; (RuntimeAccount::SIZE + token_data_len).div_ceil(8)];
2082        let token_raw = token_backing.as_mut_ptr() as *mut RuntimeAccount;
2083        // 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.
2084        unsafe {
2085            token_raw.write(RuntimeAccount {
2086                borrow_state: NOT_BORROWED,
2087                is_signer: 0,
2088                is_writable: 1,
2089                executable: 0,
2090                resize_delta: 0,
2091                address: NativeAddress::new_from_array([0xAA; 32]),
2092                owner: NativeAddress::new_from_array([3; 32]),
2093                lamports: 2_039_280,
2094                data_len: token_data_len as u64,
2095            });
2096            // Write the SPL TokenAccount.owner field at data[32..64].
2097            let data_ptr = (token_raw as *mut u8).add(RuntimeAccount::SIZE);
2098            core::ptr::copy_nonoverlapping(token_owner_bytes.as_ptr(), data_ptr.add(32), 32);
2099        }
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        let token_backend = unsafe { NativeAccountView::new_unchecked(token_raw) };
2102        let token_view = crate::account::AccountView::from_backend(token_backend);
2103
2104        // Authority: no data needed, just an address field.
2105        let mut auth_backing = std::vec![0u64; (RuntimeAccount::SIZE).div_ceil(8)];
2106        let auth_raw = auth_backing.as_mut_ptr() as *mut RuntimeAccount;
2107        // 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.
2108        unsafe {
2109            auth_raw.write(RuntimeAccount {
2110                borrow_state: NOT_BORROWED,
2111                is_signer: 1,
2112                is_writable: 0,
2113                executable: 0,
2114                resize_delta: 0,
2115                address: NativeAddress::new_from_array(authority_bytes),
2116                owner: NativeAddress::new_from_array([0; 32]),
2117                lamports: 0,
2118                data_len: 0,
2119            });
2120        }
2121        // 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.
2122        let auth_backend = unsafe { NativeAccountView::new_unchecked(auth_raw) };
2123        let auth_view = crate::account::AccountView::from_backend(auth_backend);
2124
2125        (token_backing, auth_backing, token_view, auth_view)
2126    }
2127
2128    #[test]
2129    fn require_token_authority_accepts_matching_owner() {
2130        let authority = [0x42u8; 32];
2131        let (_tb, _ab, token, auth) = make_token_and_authority(authority, authority);
2132        require_token_authority(&token, &auth).unwrap();
2133    }
2134
2135    #[test]
2136    fn require_token_authority_rejects_mismatched_owner() {
2137        let authority = [0x42u8; 32];
2138        let wrong_owner = [0x77u8; 32];
2139        let (_tb, _ab, token, auth) = make_token_and_authority(authority, wrong_owner);
2140        let err = require_token_authority(&token, &auth).unwrap_err();
2141        assert!(matches!(err, ProgramError::IncorrectAuthority));
2142    }
2143
2144    #[test]
2145    fn require_token_authority_rejects_short_buffer() {
2146        use hopper_native::{
2147            AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
2148            NOT_BORROWED,
2149        };
2150
2151        // Token account with only 50 bytes of data is not a valid
2152        // SPL TokenAccount (owner field starts at byte 32 and runs
2153        // through byte 63, so a 50-byte buffer is short).
2154        let data_len = 50;
2155        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + data_len).div_ceil(8)];
2156        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
2157        // 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.
2158        unsafe {
2159            raw.write(RuntimeAccount {
2160                borrow_state: NOT_BORROWED,
2161                is_signer: 0,
2162                is_writable: 1,
2163                executable: 0,
2164                resize_delta: 0,
2165                address: NativeAddress::new_from_array([0xAA; 32]),
2166                owner: NativeAddress::new_from_array([3; 32]),
2167                lamports: 0,
2168                data_len: data_len as u64,
2169            });
2170        }
2171        // 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.
2172        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
2173        let token = crate::account::AccountView::from_backend(backend);
2174
2175        let (_ab, _, _, auth) = make_token_and_authority([0x11; 32], [0x11; 32]);
2176        let err = require_token_authority(&token, &auth).unwrap_err();
2177        assert!(matches!(err, ProgramError::AccountDataTooSmall));
2178    }
2179
2180    // ---------------------------------------------------------------------
2181    //
2182    // These lock in the behavior that `#[account(token::mint = X)]`,
2183    // `#[account(mint::authority = Y)]`, and friends lower to. They
2184    // share the same harness as require_token_authority above, but
2185    // exercise different byte ranges of the account buffer.
2186
2187    /// Construct a valid SPL TokenAccount-shaped buffer (165 bytes)
2188    /// with both `mint` (bytes 0..32) and `owner` (bytes 32..64)
2189    /// populated to the caller's choice. Used by the token_mint /
2190    /// token_owner_eq regression tests.
2191    fn make_token_with_mint_and_owner(
2192        mint_bytes: [u8; 32],
2193        owner_bytes: [u8; 32],
2194    ) -> (std::vec::Vec<u64>, crate::account::AccountView<'static>) {
2195        use hopper_native::{
2196            AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
2197            NOT_BORROWED,
2198        };
2199
2200        let token_data_len = 165;
2201        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + token_data_len).div_ceil(8)];
2202        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
2203        // 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.
2204        unsafe {
2205            raw.write(RuntimeAccount {
2206                borrow_state: NOT_BORROWED,
2207                is_signer: 0,
2208                is_writable: 1,
2209                executable: 0,
2210                resize_delta: 0,
2211                address: NativeAddress::new_from_array([0xAA; 32]),
2212                owner: NativeAddress::new_from_array([3; 32]),
2213                lamports: 2_039_280,
2214                data_len: token_data_len as u64,
2215            });
2216            let data_ptr = (raw as *mut u8).add(RuntimeAccount::SIZE);
2217            core::ptr::copy_nonoverlapping(mint_bytes.as_ptr(), data_ptr, 32);
2218            core::ptr::copy_nonoverlapping(owner_bytes.as_ptr(), data_ptr.add(32), 32);
2219        }
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        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
2222        let view = crate::account::AccountView::from_backend(backend);
2223        (backing, view)
2224    }
2225
2226    /// Construct a valid SPL Mint-shaped buffer (82 bytes), with the
2227    /// mint_authority COption set to Some(auth), decimals populated,
2228    /// and the freeze_authority COption left empty (None).
2229    fn make_mint_with_authority_decimals(
2230        mint_authority: [u8; 32],
2231        decimals: u8,
2232    ) -> (std::vec::Vec<u64>, crate::account::AccountView<'static>) {
2233        use hopper_native::{
2234            AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
2235            NOT_BORROWED,
2236        };
2237
2238        let mint_data_len = 82;
2239        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + mint_data_len).div_ceil(8)];
2240        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
2241        // 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.
2242        unsafe {
2243            raw.write(RuntimeAccount {
2244                borrow_state: NOT_BORROWED,
2245                is_signer: 0,
2246                is_writable: 0,
2247                executable: 0,
2248                resize_delta: 0,
2249                address: NativeAddress::new_from_array([0xBB; 32]),
2250                owner: NativeAddress::new_from_array([3; 32]),
2251                lamports: 1_461_600,
2252                data_len: mint_data_len as u64,
2253            });
2254            let data_ptr = (raw as *mut u8).add(RuntimeAccount::SIZE);
2255            // mint_authority COption tag = Some (u32 LE = 1).
2256            let some_tag: [u8; 4] = 1u32.to_le_bytes();
2257            core::ptr::copy_nonoverlapping(some_tag.as_ptr(), data_ptr, 4);
2258            core::ptr::copy_nonoverlapping(mint_authority.as_ptr(), data_ptr.add(4), 32);
2259            // Supply bytes [36..44] stay zero.
2260            // Decimals at byte 44.
2261            *data_ptr.add(44) = decimals;
2262            // is_initialized byte 45 = 1.
2263            *data_ptr.add(45) = 1;
2264            // freeze_authority COption tag = None (bytes 46..50 stay zero).
2265        }
2266        // 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.
2267        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
2268        let view = crate::account::AccountView::from_backend(backend);
2269        (backing, view)
2270    }
2271
2272    #[test]
2273    fn require_token_mint_accepts_matching_mint() {
2274        let mint = [0xABu8; 32];
2275        let (_b, view) = make_token_with_mint_and_owner(mint, [0; 32]);
2276        let expected = crate::address::Address::new_from_array(mint);
2277        require_token_mint(&view, &expected).unwrap();
2278    }
2279
2280    #[test]
2281    fn require_token_mint_rejects_mismatched_mint() {
2282        let mint = [0xABu8; 32];
2283        let (_b, view) = make_token_with_mint_and_owner(mint, [0; 32]);
2284        let wrong = crate::address::Address::new_from_array([0xCDu8; 32]);
2285        let err = require_token_mint(&view, &wrong).unwrap_err();
2286        assert!(matches!(err, ProgramError::InvalidAccountData));
2287    }
2288
2289    #[test]
2290    fn require_token_owner_eq_matches() {
2291        let owner = [0x77u8; 32];
2292        let (_b, view) = make_token_with_mint_and_owner([0; 32], owner);
2293        let expected = crate::address::Address::new_from_array(owner);
2294        require_token_owner_eq(&view, &expected).unwrap();
2295    }
2296
2297    #[test]
2298    fn require_token_owner_eq_rejects_mismatch() {
2299        let owner = [0x77u8; 32];
2300        let (_b, view) = make_token_with_mint_and_owner([0; 32], owner);
2301        let wrong = crate::address::Address::new_from_array([0x88u8; 32]);
2302        let err = require_token_owner_eq(&view, &wrong).unwrap_err();
2303        assert!(matches!(err, ProgramError::IncorrectAuthority));
2304    }
2305
2306    #[test]
2307    fn require_mint_authority_accepts_matching() {
2308        let auth = [0x99u8; 32];
2309        let (_b, view) = make_mint_with_authority_decimals(auth, 6);
2310        let expected = crate::address::Address::new_from_array(auth);
2311        require_mint_authority(&view, &expected).unwrap();
2312    }
2313
2314    #[test]
2315    fn require_mint_authority_rejects_mismatched() {
2316        let auth = [0x99u8; 32];
2317        let (_b, view) = make_mint_with_authority_decimals(auth, 6);
2318        let wrong = crate::address::Address::new_from_array([0x00u8; 32]);
2319        let err = require_mint_authority(&view, &wrong).unwrap_err();
2320        assert!(matches!(err, ProgramError::IncorrectAuthority));
2321    }
2322
2323    #[test]
2324    fn require_mint_decimals_matches() {
2325        let (_b, view) = make_mint_with_authority_decimals([1u8; 32], 9);
2326        require_mint_decimals(&view, 9).unwrap();
2327    }
2328
2329    #[test]
2330    fn require_mint_decimals_rejects_mismatch() {
2331        let (_b, view) = make_mint_with_authority_decimals([1u8; 32], 9);
2332        let err = require_mint_decimals(&view, 6).unwrap_err();
2333        assert!(matches!(err, ProgramError::InvalidAccountData));
2334    }
2335
2336    #[test]
2337    fn require_mint_freeze_authority_rejects_none_tag() {
2338        // `make_mint_with_authority_decimals` deliberately leaves
2339        // freeze_authority as None. asking for a specific freeze
2340        // authority on such a mint must fail with InvalidAccountData
2341        // (not IncorrectAuthority, because the tag is the problem
2342        // rather than the pubkey bytes).
2343        let (_b, view) = make_mint_with_authority_decimals([1u8; 32], 9);
2344        let expected = crate::address::Address::new_from_array([2u8; 32]);
2345        let err = require_mint_freeze_authority(&view, &expected).unwrap_err();
2346        assert!(matches!(err, ProgramError::InvalidAccountData));
2347    }
2348}