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