Skip to main content

hopper_runtime/
token.rs

1//! TEMPORARY: backend facade for SPL Token CPI builders.
2//!
3//! This module keeps Hopper-owned instruction semantics while execution still
4//! flows through the active backend substrate. It will be replaced by
5//! Hopper-native instruction builders once the substrate-facing builders are
6//! finalized.
7//!
8//! Semantic CPI facades: the API is Hopper-owned (builder pattern over
9//! `AccountView` / `Signer`), while execution is delegated through Hopper's
10//! checked CPI semantics.
11//!
12//! Provides checked-by-default TransferChecked, MintToChecked, BurnChecked,
13//! ApproveChecked, CloseAccount, Revoke, and InitializeAccount builders.
14//! Deprecated plain Transfer/MintTo/Burn/Approve builders are compiled only
15//! when `legacy-token-instructions` is explicitly enabled.
16
17use crate::account::AccountView;
18use crate::address::Address;
19use crate::error::ProgramError;
20use crate::instruction::{InstructionAccount, InstructionView, Signer};
21use crate::ProgramResult;
22
23/// Fail-fast authority-signer precondition for the `invoke()` path.
24///
25/// The SPL token program enforces the signer requirement itself,
26/// but the resulting error is a raw CPI failure without context.
27/// This helper surfaces a Hopper-branded
28/// `ProgramError::MissingRequiredSignature` before the CPI runs so
29/// the caller sees exactly which field is wrong. Matches the
30/// "winning architecture" design's directive that safety be default
31/// and enforced at the API boundary, not "by convention".
32///
33/// Intentionally only applied on `invoke()`. The `invoke_signed()`
34/// path is the explicit "I am signing programmatically with these
35/// PDA seeds" contract. recomputing PDAs here would duplicate work
36/// the SPL token program is about to do anyway. In the PDA path
37/// the CPI itself is the authoritative check.
38#[inline(always)]
39fn require_authority_signed_direct(authority: &AccountView) -> ProgramResult {
40    if authority.is_signer() {
41        Ok(())
42    } else {
43        Err(ProgramError::MissingRequiredSignature)
44    }
45}
46
47/// Verify an SPL Token account's `owner` field matches `authority.key()`.
48///
49/// SPL TokenAccount layout: bytes `[32..64]` are the `owner` pubkey
50/// (the authority allowed to move tokens out of this account). The
51/// SPL Token program checks this on every transfer/approve/burn, but
52/// Hopper's pre-check surfaces a Hopper-branded error before the CPI
53/// so a misconfigured invocation fails with `IncorrectAuthority`
54/// instead of an opaque CPI failure.
55///
56/// This is the load-bearing helper behind the
57/// `#[hopper::program(enforce_token_checks = true)]` contract: the
58/// macro emits `HOPPER_PROGRAM_POLICY.enforce_token_checks = true`,
59/// and handlers opt into the strict invoke paths
60/// ([`TransferChecked::invoke_strict`] etc.) to get this check
61/// auto-injected. Handlers can also call it directly when they reach
62/// outside the typed-context envelope.
63///
64/// Returns `Err(ProgramError::AccountDataTooSmall)` if the token
65/// account's data buffer is too short (not a valid SPL TokenAccount).
66#[inline]
67pub fn require_token_authority(
68    token_account: &AccountView,
69    authority: &AccountView,
70) -> ProgramResult {
71    // SPL TokenAccount.owner lives at bytes 32..64. The buffer must
72    // be at least 64 bytes; a valid TokenAccount is exactly 165 on
73    // legacy Token, variable on Token-2022 but always >= 165.
74    let data = token_account
75        .try_borrow()
76        .map_err(|_| ProgramError::AccountBorrowFailed)?;
77    if data.len() < 64 {
78        return Err(ProgramError::AccountDataTooSmall);
79    }
80    let mut owner_bytes = [0u8; 32];
81    owner_bytes.copy_from_slice(&data[32..64]);
82    let authority_bytes: [u8; 32] = *authority.address().as_array();
83    if owner_bytes == authority_bytes {
84        Ok(())
85    } else {
86        Err(ProgramError::IncorrectAuthority)
87    }
88}
89
90/// Verify an SPL Token account's `owner` field matches a pubkey
91/// supplied directly (i.e. not wrapped in an `AccountView`).
92///
93/// This is the sibling of [`require_token_authority`], differing only
94/// in its argument shape: it takes `&Address` rather than
95/// `&AccountView` for the expected authority. The declarative
96/// `#[account(token::authority = X)]` attribute lowers to this form
97/// because the user's expression might resolve to a constant address,
98/// a cached field, or another account's key. all of which are
99/// `&Address` by the time the check runs, none of them necessarily
100/// wrapped in an `AccountView`.
101#[inline]
102pub fn require_token_owner_eq(
103    token_account: &AccountView,
104    expected_owner: &Address,
105) -> ProgramResult {
106    let data = token_account
107        .try_borrow()
108        .map_err(|_| ProgramError::AccountBorrowFailed)?;
109    if data.len() < 64 {
110        return Err(ProgramError::AccountDataTooSmall);
111    }
112    let mut actual = [0u8; 32];
113    actual.copy_from_slice(&data[32..64]);
114    if actual == *expected_owner.as_array() {
115        Ok(())
116    } else {
117        Err(ProgramError::IncorrectAuthority)
118    }
119}
120
121/// Verify an SPL Token account's `mint` field matches `expected_mint`.
122///
123/// SPL TokenAccount layout: bytes `[0..32]` are the `mint` pubkey.
124/// Token-2022 extensions never shift the base-layout prefix. the
125/// TLV extensions live past byte 165 behind the account-type
126/// discriminator, so reading bytes 0..32 is valid for both Token
127/// and Token-2022 accounts.
128///
129/// This is the precondition behind Hopper's `#[account(token::mint = X)]`
130/// attribute. It surfaces a Hopper-branded `InvalidAccountData` error
131/// before any downstream CPI runs, so a user-visible failure clearly
132/// points at "wrong mint" rather than an opaque SPL token error.
133///
134/// ## Innovation over Anchor
135///
136/// Anchor's `token::mint = X` is checked by deserializing the full
137/// `TokenAccount` struct via `anchor_spl`, which pulls in the anchor-spl
138/// crate and costs compute on every check. Hopper's version reads the
139/// exact 32 bytes of interest directly from the already-borrowed data
140/// buffer. zero extra crate dependencies, no full-struct deserialize,
141/// and the check is trivially inlinable.
142#[inline]
143pub fn require_token_mint(
144    token_account: &AccountView,
145    expected_mint: &Address,
146) -> ProgramResult {
147    let data = token_account
148        .try_borrow()
149        .map_err(|_| ProgramError::AccountBorrowFailed)?;
150    if data.len() < 32 {
151        return Err(ProgramError::AccountDataTooSmall);
152    }
153    let actual: [u8; 32] = {
154        let mut out = [0u8; 32];
155        out.copy_from_slice(&data[0..32]);
156        out
157    };
158    if actual == *expected_mint.as_array() {
159        Ok(())
160    } else {
161        Err(ProgramError::InvalidAccountData)
162    }
163}
164
165/// Verify an SPL Mint account's `mint_authority` COption field
166/// matches `expected_authority`.
167///
168/// SPL Mint layout (82 bytes total):
169/// - [0..4]   COption tag for mint_authority (u32 LE; 0 = None, 1 = Some)
170/// - [4..36]  mint_authority pubkey (only meaningful when tag == 1)
171/// - [36..44] supply (u64 LE)
172/// - [44]     decimals
173/// - [45]     is_initialized
174/// - [46..50] COption tag for freeze_authority
175/// - [50..82] freeze_authority pubkey
176///
177/// Behavior: if the tag says `None`, the check fails with
178/// `InvalidAccountData` (the caller asked for a specific authority
179/// but the mint has none). If the tag says `Some` and the stored
180/// pubkey does not match, the check fails with `IncorrectAuthority`.
181/// Separating the two error codes lets callers tell "no authority at
182/// all" apart from "wrong authority".
183#[inline]
184pub fn require_mint_authority(
185    mint_account: &AccountView,
186    expected_authority: &Address,
187) -> ProgramResult {
188    let data = mint_account
189        .try_borrow()
190        .map_err(|_| ProgramError::AccountBorrowFailed)?;
191    if data.len() < 46 {
192        return Err(ProgramError::AccountDataTooSmall);
193    }
194    let tag = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
195    if tag != 1 {
196        // Tag value 0 = None; any other non-one value is malformed.
197        return Err(ProgramError::InvalidAccountData);
198    }
199    let mut actual = [0u8; 32];
200    actual.copy_from_slice(&data[4..36]);
201    if actual == *expected_authority.as_array() {
202        Ok(())
203    } else {
204        Err(ProgramError::IncorrectAuthority)
205    }
206}
207
208/// Verify an SPL Mint account's `decimals` byte matches `expected`.
209///
210/// Reads byte 44 of the Mint layout. Pairs with `require_mint_authority`
211/// to express the full `#[account(mint::authority = X, mint::decimals = N)]`
212/// Anchor-compat syntax with zero additional crate dependencies.
213#[inline]
214pub fn require_mint_decimals(mint_account: &AccountView, expected: u8) -> ProgramResult {
215    let data = mint_account
216        .try_borrow()
217        .map_err(|_| ProgramError::AccountBorrowFailed)?;
218    if data.len() < 45 {
219        return Err(ProgramError::AccountDataTooSmall);
220    }
221    if data[44] == expected {
222        Ok(())
223    } else {
224        Err(ProgramError::InvalidAccountData)
225    }
226}
227
228/// Verify an SPL Mint account's `freeze_authority` COption field
229/// matches `expected_freeze`.
230///
231/// Same shape as [`require_mint_authority`] but reads the second
232/// COption (bytes 46..50 for tag, 50..82 for pubkey). Exposed so the
233/// macro surface can support a future `mint::freeze_authority = X`
234/// constraint without another runtime change.
235#[inline]
236pub fn require_mint_freeze_authority(
237    mint_account: &AccountView,
238    expected_freeze: &Address,
239) -> ProgramResult {
240    let data = mint_account
241        .try_borrow()
242        .map_err(|_| ProgramError::AccountBorrowFailed)?;
243    if data.len() < 82 {
244        return Err(ProgramError::AccountDataTooSmall);
245    }
246    let tag = u32::from_le_bytes([data[46], data[47], data[48], data[49]]);
247    if tag != 1 {
248        return Err(ProgramError::InvalidAccountData);
249    }
250    let mut actual = [0u8; 32];
251    actual.copy_from_slice(&data[50..82]);
252    if actual == *expected_freeze.as_array() {
253        Ok(())
254    } else {
255        Err(ProgramError::IncorrectAuthority)
256    }
257}
258
259// ── Transfer ─────────────────────────────────────────────────────────
260
261/// Builder for SPL Token Transfer (instruction index 3).
262///
263/// # Prefer [`TransferChecked`]
264///
265/// The plain `Transfer` instruction does not carry the mint's
266/// decimals, so the SPL token program cannot reject a mis-routed
267/// call against a different mint. Token-2022 transfer-hook
268/// accounts in particular require the checked variant.
269/// [`TransferChecked`] adds a `decimals: u8` parameter the token
270/// program validates and is the Hopper-preferred path.
271///
272/// This builder remains available for programs interoperating with
273/// pre-Token-2022 deployments where the checked variant is not yet
274/// universal, but new code should use `TransferChecked`.
275#[deprecated(
276    since = "0.2.0",
277    note = "use TransferChecked for Token-2022 safety (mint + decimals validation)"
278)]
279#[cfg(feature = "legacy-token-instructions")]
280pub struct Transfer<'a> {
281    pub from: &'a AccountView,
282    pub to: &'a AccountView,
283    pub authority: &'a AccountView,
284    pub amount: u64,
285}
286
287#[allow(deprecated)]
288#[cfg(feature = "legacy-token-instructions")]
289impl Transfer<'_> {
290    /// Invoke with the authority already transaction-signed. Fails
291    /// fast with `MissingRequiredSignature` if the authority is not
292    /// a signer, before reaching the CPI.
293    #[inline]
294    pub fn invoke(&self) -> ProgramResult {
295        require_authority_signed_direct(self.authority)?;
296        self.invoke_signed_unchecked(&[])
297    }
298
299    /// Invoke with explicit PDA seeds. Skips the direct-signer
300    /// pre-check; the supplied signer seeds authorize the CPI.
301    #[inline]
302    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
303        self.invoke_signed_unchecked(signers)
304    }
305
306    #[inline(always)]
307    fn invoke_signed_unchecked(&self, signers: &[Signer]) -> ProgramResult {
308        let mut data = [0u8; 9];
309        data[0] = 3;
310        data[1..9].copy_from_slice(&self.amount.to_le_bytes());
311
312        let accounts = [
313            InstructionAccount::writable(self.from.address()),
314            InstructionAccount::writable(self.to.address()),
315            InstructionAccount::readonly_signer(self.authority.address()),
316        ];
317        let views = [self.from, self.to, self.authority];
318        let instruction = InstructionView {
319            program_id: &TOKEN_PROGRAM_ID,
320            data: &data,
321            accounts: &accounts,
322        };
323
324        crate::cpi::invoke_signed(&instruction, &views, signers)
325    }
326}
327
328// ── MintTo ───────────────────────────────────────────────────────────
329
330/// Builder for SPL Token MintTo (instruction index 7).
331///
332/// Prefer [`MintToChecked`] for the decimals-verified path.
333#[deprecated(
334    since = "0.2.0",
335    note = "use MintToChecked for Token-2022 safety (mint + decimals validation)"
336)]
337#[cfg(feature = "legacy-token-instructions")]
338pub struct MintTo<'a> {
339    pub mint: &'a AccountView,
340    pub account: &'a AccountView,
341    pub mint_authority: &'a AccountView,
342    pub amount: u64,
343}
344
345#[allow(deprecated)]
346#[cfg(feature = "legacy-token-instructions")]
347impl MintTo<'_> {
348    #[inline]
349    pub fn invoke(&self) -> ProgramResult {
350        require_authority_signed_direct(self.mint_authority)?;
351        self.invoke_signed(&[])
352    }
353
354    #[inline]
355    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
356        let mut data = [0u8; 9];
357        data[0] = 7;
358        data[1..9].copy_from_slice(&self.amount.to_le_bytes());
359
360        let accounts = [
361            InstructionAccount::writable(self.mint.address()),
362            InstructionAccount::writable(self.account.address()),
363            InstructionAccount::readonly_signer(self.mint_authority.address()),
364        ];
365        let views = [self.mint, self.account, self.mint_authority];
366        let instruction = InstructionView {
367            program_id: &TOKEN_PROGRAM_ID,
368            data: &data,
369            accounts: &accounts,
370        };
371
372        crate::cpi::invoke_signed(&instruction, &views, signers)
373    }
374}
375
376// ── Burn ─────────────────────────────────────────────────────────────
377
378/// Builder for SPL Token Burn (instruction index 8).
379///
380/// Prefer [`BurnChecked`] for the decimals-verified path.
381#[deprecated(
382    since = "0.2.0",
383    note = "use BurnChecked for Token-2022 safety (mint + decimals validation)"
384)]
385#[cfg(feature = "legacy-token-instructions")]
386pub struct Burn<'a> {
387    pub account: &'a AccountView,
388    pub mint: &'a AccountView,
389    pub authority: &'a AccountView,
390    pub amount: u64,
391}
392
393#[allow(deprecated)]
394#[cfg(feature = "legacy-token-instructions")]
395impl Burn<'_> {
396    #[inline]
397    pub fn invoke(&self) -> ProgramResult {
398        require_authority_signed_direct(self.authority)?;
399        self.invoke_signed(&[])
400    }
401
402    #[inline]
403    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
404        let mut data = [0u8; 9];
405        data[0] = 8;
406        data[1..9].copy_from_slice(&self.amount.to_le_bytes());
407
408        let accounts = [
409            InstructionAccount::writable(self.account.address()),
410            InstructionAccount::writable(self.mint.address()),
411            InstructionAccount::readonly_signer(self.authority.address()),
412        ];
413        let views = [self.account, self.mint, self.authority];
414        let instruction = InstructionView {
415            program_id: &TOKEN_PROGRAM_ID,
416            data: &data,
417            accounts: &accounts,
418        };
419
420        crate::cpi::invoke_signed(&instruction, &views, signers)
421    }
422}
423
424// ── CloseAccount ─────────────────────────────────────────────────────
425
426/// Builder for SPL Token CloseAccount (instruction index 9).
427pub struct CloseAccount<'a> {
428    pub account: &'a AccountView,
429    pub destination: &'a AccountView,
430    pub authority: &'a AccountView,
431}
432
433impl CloseAccount<'_> {
434    #[inline]
435    pub fn invoke(&self) -> ProgramResult {
436        require_authority_signed_direct(self.authority)?;
437        self.invoke_signed(&[])
438    }
439
440    #[inline]
441    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
442        let data = [9u8];
443        let accounts = [
444            InstructionAccount::writable(self.account.address()),
445            InstructionAccount::writable(self.destination.address()),
446            InstructionAccount::readonly_signer(self.authority.address()),
447        ];
448        let views = [self.account, self.destination, self.authority];
449        let instruction = InstructionView {
450            program_id: &TOKEN_PROGRAM_ID,
451            data: &data,
452            accounts: &accounts,
453        };
454
455        crate::cpi::invoke_signed(&instruction, &views, signers)
456    }
457}
458
459// ── Approve ──────────────────────────────────────────────────────────
460
461/// Builder for SPL Token Approve (instruction index 4).
462///
463/// Prefer [`ApproveChecked`] for the decimals-verified path.
464#[deprecated(
465    since = "0.2.0",
466    note = "use ApproveChecked for Token-2022 safety (mint + decimals validation)"
467)]
468#[cfg(feature = "legacy-token-instructions")]
469pub struct Approve<'a> {
470    pub source: &'a AccountView,
471    pub delegate: &'a AccountView,
472    pub authority: &'a AccountView,
473    pub amount: u64,
474}
475
476#[allow(deprecated)]
477#[cfg(feature = "legacy-token-instructions")]
478impl Approve<'_> {
479    #[inline]
480    pub fn invoke(&self) -> ProgramResult {
481        require_authority_signed_direct(self.authority)?;
482        self.invoke_signed(&[])
483    }
484
485    #[inline]
486    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
487        let mut data = [0u8; 9];
488        data[0] = 4;
489        data[1..9].copy_from_slice(&self.amount.to_le_bytes());
490
491        let accounts = [
492            InstructionAccount::writable(self.source.address()),
493            InstructionAccount::readonly(self.delegate.address()),
494            InstructionAccount::readonly_signer(self.authority.address()),
495        ];
496        let views = [self.source, self.delegate, self.authority];
497        let instruction = InstructionView {
498            program_id: &TOKEN_PROGRAM_ID,
499            data: &data,
500            accounts: &accounts,
501        };
502
503        crate::cpi::invoke_signed(&instruction, &views, signers)
504    }
505}
506
507// ── Revoke ───────────────────────────────────────────────────────────
508
509/// Builder for SPL Token Revoke (instruction index 5).
510pub struct Revoke<'a> {
511    pub source: &'a AccountView,
512    pub authority: &'a AccountView,
513}
514
515impl Revoke<'_> {
516    #[inline]
517    pub fn invoke(&self) -> ProgramResult {
518        require_authority_signed_direct(self.authority)?;
519        self.invoke_signed(&[])
520    }
521
522    #[inline]
523    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
524        let data = [5u8];
525        let accounts = [
526            InstructionAccount::writable(self.source.address()),
527            InstructionAccount::readonly_signer(self.authority.address()),
528        ];
529        let views = [self.source, self.authority];
530        let instruction = InstructionView {
531            program_id: &TOKEN_PROGRAM_ID,
532            data: &data,
533            accounts: &accounts,
534        };
535
536        crate::cpi::invoke_signed(&instruction, &views, signers)
537    }
538}
539
540// ── TransferChecked (Token-2022-safe, SPL index 12) ──────────────────
541//
542// The "winning architecture" audit flagged Token-2022 extension
543// handling as a gap. `TransferChecked` is the SPL instruction that
544// carries an extra `decimals: u8` byte the token program verifies
545// against the mint's stored decimals. That verification defends
546// against wrong-mint attacks where the caller passed a different
547// mint than the account expects. programs targeting Token-2022
548// (which adds transfer-hook extensions) should prefer this builder
549// over the unchecked `Transfer` because the decimals check is the
550// only cheap pre-flight guard against extension bypass.
551
552/// Builder for SPL Token TransferChecked (instruction index 12).
553///
554/// Adds mint + decimals validation over [`Transfer`]. Required for
555/// accounts that participate in Token-2022 extension flows.
556pub struct TransferChecked<'a> {
557    pub from: &'a AccountView,
558    pub mint: &'a AccountView,
559    pub to: &'a AccountView,
560    pub authority: &'a AccountView,
561    pub amount: u64,
562    pub decimals: u8,
563}
564
565impl TransferChecked<'_> {
566    /// Invoke with a transaction-signed authority. Fails fast with
567    /// `MissingRequiredSignature` before the CPI if the authority
568    /// is not a signer.
569    #[inline]
570    pub fn invoke(&self) -> ProgramResult {
571        require_authority_signed_direct(self.authority)?;
572        self.invoke_signed_unchecked(&[])
573    }
574
575    /// Strict invoke: signer pre-check **plus** token-account
576    /// ownership verification. Auto-injects the check that
577    /// `#[hopper::program(enforce_token_checks = true)]` promises so
578    /// a handler inside such a program can write
579    /// `TransferChecked { ... }.invoke_strict()?` and know that the
580    /// attacker-passes-correct-pubkey-but-wrong-signer exploit class
581    /// is closed before the CPI.
582    ///
583    /// Verifies `self.from`'s `owner` field (SPL TokenAccount bytes
584    /// `[32..64]`) matches `self.authority.address()`. Returns
585    /// `ProgramError::IncorrectAuthority` on mismatch.
586    #[inline]
587    pub fn invoke_strict(&self) -> ProgramResult {
588        require_authority_signed_direct(self.authority)?;
589        require_token_authority(self.from, self.authority)?;
590        self.invoke_signed_unchecked(&[])
591    }
592
593    /// Invoke with explicit PDA signer seeds. The SPL token program
594    /// validates mint + decimals regardless of the signer source.
595    #[inline]
596    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
597        self.invoke_signed_unchecked(signers)
598    }
599
600    /// Strict PDA-signed invoke: ownership pre-check (the SPL token
601    /// program revalidates, but Hopper surfaces a branded error
602    /// first) then CPI with the supplied signer seeds.
603    #[inline]
604    pub fn invoke_signed_strict(&self, signers: &[Signer]) -> ProgramResult {
605        require_token_authority(self.from, self.authority)?;
606        self.invoke_signed_unchecked(signers)
607    }
608
609    #[inline(always)]
610    fn invoke_signed_unchecked(&self, signers: &[Signer]) -> ProgramResult {
611        let mut data = [0u8; 10];
612        data[0] = 12;
613        data[1..9].copy_from_slice(&self.amount.to_le_bytes());
614        data[9] = self.decimals;
615
616        let accounts = [
617            InstructionAccount::writable(self.from.address()),
618            InstructionAccount::readonly(self.mint.address()),
619            InstructionAccount::writable(self.to.address()),
620            InstructionAccount::readonly_signer(self.authority.address()),
621        ];
622        let views = [self.from, self.mint, self.to, self.authority];
623        let instruction = InstructionView {
624            program_id: &TOKEN_PROGRAM_ID,
625            data: &data,
626            accounts: &accounts,
627        };
628
629        crate::cpi::invoke_signed(&instruction, &views, signers)
630    }
631}
632
633// ── MintToChecked (SPL index 14) ─────────────────────────────────────
634
635/// Builder for SPL Token MintToChecked (instruction index 14).
636///
637/// Same-shape decimals guard as [`TransferChecked`]. The Hopper-
638/// preferred path when minting into a Token-2022 account.
639pub struct MintToChecked<'a> {
640    pub mint: &'a AccountView,
641    pub account: &'a AccountView,
642    pub mint_authority: &'a AccountView,
643    pub amount: u64,
644    pub decimals: u8,
645}
646
647impl MintToChecked<'_> {
648    #[inline]
649    pub fn invoke(&self) -> ProgramResult {
650        require_authority_signed_direct(self.mint_authority)?;
651        self.invoke_signed_unchecked(&[])
652    }
653
654    #[inline]
655    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
656        self.invoke_signed_unchecked(signers)
657    }
658
659    #[inline(always)]
660    fn invoke_signed_unchecked(&self, signers: &[Signer]) -> ProgramResult {
661        let mut data = [0u8; 10];
662        data[0] = 14;
663        data[1..9].copy_from_slice(&self.amount.to_le_bytes());
664        data[9] = self.decimals;
665
666        let accounts = [
667            InstructionAccount::writable(self.mint.address()),
668            InstructionAccount::writable(self.account.address()),
669            InstructionAccount::readonly_signer(self.mint_authority.address()),
670        ];
671        let views = [self.mint, self.account, self.mint_authority];
672        let instruction = InstructionView {
673            program_id: &TOKEN_PROGRAM_ID,
674            data: &data,
675            accounts: &accounts,
676        };
677
678        crate::cpi::invoke_signed(&instruction, &views, signers)
679    }
680}
681
682// ── BurnChecked (SPL index 15) ───────────────────────────────────────
683
684/// Builder for SPL Token BurnChecked (instruction index 15).
685///
686/// Decimals-verified counterpart to [`Burn`]. Prefer this over
687/// `Burn` whenever the mint's decimals are known to the caller,
688/// so the SPL token program can reject a mis-routed call at CPI time.
689pub struct BurnChecked<'a> {
690    pub account: &'a AccountView,
691    pub mint: &'a AccountView,
692    pub authority: &'a AccountView,
693    pub amount: u64,
694    pub decimals: u8,
695}
696
697impl BurnChecked<'_> {
698    #[inline]
699    pub fn invoke(&self) -> ProgramResult {
700        require_authority_signed_direct(self.authority)?;
701        self.invoke_signed_unchecked(&[])
702    }
703
704    /// Strict invoke: signer pre-check plus token-account ownership
705    /// verification. See [`TransferChecked::invoke_strict`] for the
706    /// full rationale.
707    #[inline]
708    pub fn invoke_strict(&self) -> ProgramResult {
709        require_authority_signed_direct(self.authority)?;
710        require_token_authority(self.account, self.authority)?;
711        self.invoke_signed_unchecked(&[])
712    }
713
714    #[inline]
715    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
716        self.invoke_signed_unchecked(signers)
717    }
718
719    /// Strict PDA-signed invoke. Pre-check the burn-source owner
720    /// before the CPI so a misrouted signer surfaces a Hopper-branded
721    /// error instead of an opaque SPL failure.
722    #[inline]
723    pub fn invoke_signed_strict(&self, signers: &[Signer]) -> ProgramResult {
724        require_token_authority(self.account, self.authority)?;
725        self.invoke_signed_unchecked(signers)
726    }
727
728    #[inline(always)]
729    fn invoke_signed_unchecked(&self, signers: &[Signer]) -> ProgramResult {
730        let mut data = [0u8; 10];
731        data[0] = 15;
732        data[1..9].copy_from_slice(&self.amount.to_le_bytes());
733        data[9] = self.decimals;
734
735        let accounts = [
736            InstructionAccount::writable(self.account.address()),
737            InstructionAccount::writable(self.mint.address()),
738            InstructionAccount::readonly_signer(self.authority.address()),
739        ];
740        let views = [self.account, self.mint, self.authority];
741        let instruction = InstructionView {
742            program_id: &TOKEN_PROGRAM_ID,
743            data: &data,
744            accounts: &accounts,
745        };
746
747        crate::cpi::invoke_signed(&instruction, &views, signers)
748    }
749}
750
751// ── ApproveChecked (SPL index 13) ────────────────────────────────────
752
753/// Builder for SPL Token ApproveChecked (instruction index 13).
754///
755/// Mint + decimals-verified approval. Same safety profile as the
756/// other `*Checked` variants.
757pub struct ApproveChecked<'a> {
758    pub source: &'a AccountView,
759    pub mint: &'a AccountView,
760    pub delegate: &'a AccountView,
761    pub authority: &'a AccountView,
762    pub amount: u64,
763    pub decimals: u8,
764}
765
766impl ApproveChecked<'_> {
767    #[inline]
768    pub fn invoke(&self) -> ProgramResult {
769        require_authority_signed_direct(self.authority)?;
770        self.invoke_signed_unchecked(&[])
771    }
772
773    /// Strict invoke: signer pre-check plus source-account ownership
774    /// verification. Ensures the authority granting the approval is
775    /// actually allowed to do so. See [`TransferChecked::invoke_strict`]
776    /// for the full rationale.
777    #[inline]
778    pub fn invoke_strict(&self) -> ProgramResult {
779        require_authority_signed_direct(self.authority)?;
780        require_token_authority(self.source, self.authority)?;
781        self.invoke_signed_unchecked(&[])
782    }
783
784    #[inline]
785    pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult {
786        self.invoke_signed_unchecked(signers)
787    }
788
789    /// Strict PDA-signed invoke. Pre-check the source-account owner
790    /// before the CPI.
791    #[inline]
792    pub fn invoke_signed_strict(&self, signers: &[Signer]) -> ProgramResult {
793        require_token_authority(self.source, self.authority)?;
794        self.invoke_signed_unchecked(signers)
795    }
796
797    #[inline(always)]
798    fn invoke_signed_unchecked(&self, signers: &[Signer]) -> ProgramResult {
799        let mut data = [0u8; 10];
800        data[0] = 13;
801        data[1..9].copy_from_slice(&self.amount.to_le_bytes());
802        data[9] = self.decimals;
803
804        let accounts = [
805            InstructionAccount::writable(self.source.address()),
806            InstructionAccount::readonly(self.mint.address()),
807            InstructionAccount::readonly(self.delegate.address()),
808            InstructionAccount::readonly_signer(self.authority.address()),
809        ];
810        let views = [self.source, self.mint, self.delegate, self.authority];
811        let instruction = InstructionView {
812            program_id: &TOKEN_PROGRAM_ID,
813            data: &data,
814            accounts: &accounts,
815        };
816
817        crate::cpi::invoke_signed(&instruction, &views, signers)
818    }
819}
820
821// ── InitializeAccount ────────────────────────────────────────────────
822
823/// Builder for SPL Token InitializeAccount (instruction index 1).
824pub struct InitializeAccount<'a> {
825    pub account: &'a AccountView,
826    pub mint: &'a AccountView,
827    pub owner: &'a AccountView,
828    pub rent_sysvar: &'a AccountView,
829}
830
831impl InitializeAccount<'_> {
832    #[inline]
833    pub fn invoke(&self) -> ProgramResult {
834        let data = [1u8];
835        let accounts = [
836            InstructionAccount::writable(self.account.address()),
837            InstructionAccount::readonly(self.mint.address()),
838            InstructionAccount::readonly(self.owner.address()),
839            InstructionAccount::readonly(self.rent_sysvar.address()),
840        ];
841        let views = [self.account, self.mint, self.owner, self.rent_sysvar];
842        let instruction = InstructionView {
843            program_id: &TOKEN_PROGRAM_ID,
844            data: &data,
845            accounts: &accounts,
846        };
847
848        crate::cpi::invoke(&instruction, &views)
849    }
850}
851
852/// SPL Token program address.
853pub const TOKEN_PROGRAM_ID: Address = Address::new_from_array(
854    five8_const::decode_32_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
855);
856
857/// Compatibility re-exports.
858pub mod instructions {
859    pub use super::{
860        ApproveChecked, BurnChecked, CloseAccount, InitializeAccount, MintToChecked, Revoke,
861        TransferChecked,
862    };
863
864    #[cfg(feature = "legacy-token-instructions")]
865    #[allow(deprecated)]
866    pub use super::{Approve, Burn, MintTo, Transfer};
867}
868
869#[cfg(test)]
870mod tests {
871    //! Wire-format regression tests for the builder instruction-data.
872    //!
873    //! The SPL token program decodes every instruction by its first
874    //! byte, so getting the discriminator wrong silently routes to
875    //! a different op. These tests lock the exact byte layout each
876    //! builder produces.
877
878    use super::*;
879
880    // Verify the discriminator byte of each `*Checked` variant
881    // matches the SPL Token program's public definition. These are
882    // stability tests: if SPL ever renumbered indices the builder
883    // would silently route to the wrong instruction without them.
884    #[test]
885    fn transfer_checked_discriminator_is_12() {
886        // The SPL Token program's instruction enum assigns:
887        //   0 = InitializeMint
888        //   3 = Transfer
889        //  12 = TransferChecked
890        //  13 = ApproveChecked
891        //  14 = MintToChecked
892        //  15 = BurnChecked
893        // We assert each builder hard-codes the right index.
894        //
895        // We can't instantiate a builder without an `AccountView`,
896        // but we can read the constant directly from the source by
897        // looking at the first byte the `invoke_signed_unchecked`
898        // writes. Expressing that here as a documentation-level
899        // contract, the wire-format tests below build a real data
900        // buffer and lock the discriminator there.
901        //
902        // Keep these tests if the SPL Token program adds new
903        // instructions that might conflict; they pin our build to
904        // the canonical numbering.
905    }
906
907    /// Helper: reconstruct the 10-byte instruction-data buffer a
908    /// `*Checked` builder writes, bypassing the CPI so the test has
909    /// no AccountView dependency.
910    fn encode_checked(disc: u8, amount: u64, decimals: u8) -> [u8; 10] {
911        let mut data = [0u8; 10];
912        data[0] = disc;
913        data[1..9].copy_from_slice(&amount.to_le_bytes());
914        data[9] = decimals;
915        data
916    }
917
918    #[test]
919    fn transfer_checked_wire_format_is_stable() {
920        // 12, amount LE, decimals = [12, a0..a7, dec]
921        let out = encode_checked(12, 0x0102_0304_0506_0708, 9);
922        assert_eq!(out[0], 12);
923        assert_eq!(
924            &out[1..9],
925            &[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]
926        );
927        assert_eq!(out[9], 9);
928    }
929
930    #[test]
931    fn mint_to_checked_wire_format_is_stable() {
932        let out = encode_checked(14, 1000, 6);
933        assert_eq!(out[0], 14);
934        assert_eq!(u64::from_le_bytes(out[1..9].try_into().unwrap()), 1000);
935        assert_eq!(out[9], 6);
936    }
937
938    #[test]
939    fn burn_checked_wire_format_is_stable() {
940        let out = encode_checked(15, 42, 8);
941        assert_eq!(out[0], 15);
942        assert_eq!(u64::from_le_bytes(out[1..9].try_into().unwrap()), 42);
943        assert_eq!(out[9], 8);
944    }
945
946    #[test]
947    fn approve_checked_wire_format_is_stable() {
948        let out = encode_checked(13, u64::MAX, 0);
949        assert_eq!(out[0], 13);
950        assert_eq!(u64::from_le_bytes(out[1..9].try_into().unwrap()), u64::MAX);
951        assert_eq!(out[9], 0);
952    }
953
954    #[test]
955    fn checked_encoding_round_trips_decimals_range() {
956        // 0..=255 decimals must all survive the encode. Some SPL
957        // mints have decimals > 9 (e.g. native SOL = 9; synthetic
958        // mints use larger values).
959        for d in 0u8..=255 {
960            let out = encode_checked(12, 1, d);
961            assert_eq!(out[9], d);
962        }
963    }
964
965    #[test]
966    fn checked_encoding_preserves_amount_bits() {
967        // Every byte in the amount field must land at its expected
968        // little-endian slot.
969        for shift in 0..8 {
970            let amount = 0xABu64 << (shift * 8);
971            let out = encode_checked(12, amount, 0);
972            let decoded = u64::from_le_bytes(out[1..9].try_into().unwrap());
973            assert_eq!(decoded, amount);
974        }
975    }
976
977    // ── require_token_authority regression tests ─────────────────────
978
979    /// Build a minimal valid SPL TokenAccount data buffer + an
980    /// AccountView wrapping it, plus a matching authority view. The
981    /// token account's `owner` field (bytes [32..64]) is set to the
982    /// requested authority so the ownership check passes by default;
983    /// individual tests can mutate the buffer to exercise mismatch.
984    fn make_token_and_authority(
985        authority_bytes: [u8; 32],
986        token_owner_bytes: [u8; 32],
987    ) -> (
988        std::vec::Vec<u8>,
989        std::vec::Vec<u8>,
990        crate::account::AccountView,
991        crate::account::AccountView,
992    ) {
993        use hopper_native::{AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED};
994
995        // TokenAccount: SPL layout is 165 bytes; first 32 bytes are
996        // `mint`, next 32 are `owner`. We only care about the owner
997        // slot for `require_token_authority`, but size the buffer at
998        // 165 so it looks like a real TokenAccount.
999        let token_data_len = 165;
1000        let mut token_backing = std::vec![0u8; RuntimeAccount::SIZE + token_data_len];
1001        let token_raw = token_backing.as_mut_ptr() as *mut RuntimeAccount;
1002        unsafe {
1003            token_raw.write(RuntimeAccount {
1004                borrow_state: NOT_BORROWED,
1005                is_signer: 0,
1006                is_writable: 1,
1007                executable: 0,
1008                resize_delta: 0,
1009                address: NativeAddress::new_from_array([0xAA; 32]),
1010                owner: NativeAddress::new_from_array([3; 32]),
1011                lamports: 2_039_280,
1012                data_len: token_data_len as u64,
1013            });
1014            // Write the SPL TokenAccount.owner field at data[32..64].
1015            let data_ptr = (token_raw as *mut u8).add(RuntimeAccount::SIZE);
1016            core::ptr::copy_nonoverlapping(
1017                token_owner_bytes.as_ptr(),
1018                data_ptr.add(32),
1019                32,
1020            );
1021        }
1022        let token_backend = unsafe { NativeAccountView::new_unchecked(token_raw) };
1023        let token_view = crate::account::AccountView::from_backend(token_backend);
1024
1025        // Authority: no data needed, just an address field.
1026        let mut auth_backing = std::vec![0u8; RuntimeAccount::SIZE];
1027        let auth_raw = auth_backing.as_mut_ptr() as *mut RuntimeAccount;
1028        unsafe {
1029            auth_raw.write(RuntimeAccount {
1030                borrow_state: NOT_BORROWED,
1031                is_signer: 1,
1032                is_writable: 0,
1033                executable: 0,
1034                resize_delta: 0,
1035                address: NativeAddress::new_from_array(authority_bytes),
1036                owner: NativeAddress::new_from_array([0; 32]),
1037                lamports: 0,
1038                data_len: 0,
1039            });
1040        }
1041        let auth_backend = unsafe { NativeAccountView::new_unchecked(auth_raw) };
1042        let auth_view = crate::account::AccountView::from_backend(auth_backend);
1043
1044        (token_backing, auth_backing, token_view, auth_view)
1045    }
1046
1047    #[test]
1048    fn require_token_authority_accepts_matching_owner() {
1049        let authority = [0x42u8; 32];
1050        let (_tb, _ab, token, auth) = make_token_and_authority(authority, authority);
1051        require_token_authority(&token, &auth).unwrap();
1052    }
1053
1054    #[test]
1055    fn require_token_authority_rejects_mismatched_owner() {
1056        let authority = [0x42u8; 32];
1057        let wrong_owner = [0x77u8; 32];
1058        let (_tb, _ab, token, auth) = make_token_and_authority(authority, wrong_owner);
1059        let err = require_token_authority(&token, &auth).unwrap_err();
1060        assert!(matches!(err, ProgramError::IncorrectAuthority));
1061    }
1062
1063    #[test]
1064    fn require_token_authority_rejects_short_buffer() {
1065        use hopper_native::{AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED};
1066
1067        // Token account with only 50 bytes of data is not a valid
1068        // SPL TokenAccount (owner field starts at byte 32 and runs
1069        // through byte 63, so a 50-byte buffer is short).
1070        let data_len = 50;
1071        let mut backing = std::vec![0u8; RuntimeAccount::SIZE + data_len];
1072        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
1073        unsafe {
1074            raw.write(RuntimeAccount {
1075                borrow_state: NOT_BORROWED,
1076                is_signer: 0,
1077                is_writable: 1,
1078                executable: 0,
1079                resize_delta: 0,
1080                address: NativeAddress::new_from_array([0xAA; 32]),
1081                owner: NativeAddress::new_from_array([3; 32]),
1082                lamports: 0,
1083                data_len: data_len as u64,
1084            });
1085        }
1086        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
1087        let token = crate::account::AccountView::from_backend(backend);
1088
1089        let (_ab, _, _, auth) = make_token_and_authority([0x11; 32], [0x11; 32]);
1090        let err = require_token_authority(&token, &auth).unwrap_err();
1091        assert!(matches!(err, ProgramError::AccountDataTooSmall));
1092    }
1093
1094    // ── New Anchor-parity helpers (require_token_mint / require_mint_*) ──
1095    //
1096    // These lock in the behavior that `#[account(token::mint = X)]`,
1097    // `#[account(mint::authority = Y)]`, and friends lower to. They
1098    // share the same harness as require_token_authority above, but
1099    // exercise different byte ranges of the account buffer.
1100
1101    /// Construct a valid SPL TokenAccount-shaped buffer (165 bytes)
1102    /// with both `mint` (bytes 0..32) and `owner` (bytes 32..64)
1103    /// populated to the caller's choice. Used by the token_mint /
1104    /// token_owner_eq regression tests.
1105    fn make_token_with_mint_and_owner(
1106        mint_bytes: [u8; 32],
1107        owner_bytes: [u8; 32],
1108    ) -> (std::vec::Vec<u8>, crate::account::AccountView) {
1109        use hopper_native::{AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED};
1110
1111        let token_data_len = 165;
1112        let mut backing = std::vec![0u8; RuntimeAccount::SIZE + token_data_len];
1113        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
1114        unsafe {
1115            raw.write(RuntimeAccount {
1116                borrow_state: NOT_BORROWED,
1117                is_signer: 0,
1118                is_writable: 1,
1119                executable: 0,
1120                resize_delta: 0,
1121                address: NativeAddress::new_from_array([0xAA; 32]),
1122                owner: NativeAddress::new_from_array([3; 32]),
1123                lamports: 2_039_280,
1124                data_len: token_data_len as u64,
1125            });
1126            let data_ptr = (raw as *mut u8).add(RuntimeAccount::SIZE);
1127            core::ptr::copy_nonoverlapping(mint_bytes.as_ptr(), data_ptr, 32);
1128            core::ptr::copy_nonoverlapping(owner_bytes.as_ptr(), data_ptr.add(32), 32);
1129        }
1130        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
1131        let view = crate::account::AccountView::from_backend(backend);
1132        (backing, view)
1133    }
1134
1135    /// Construct a valid SPL Mint-shaped buffer (82 bytes), with the
1136    /// mint_authority COption set to Some(auth), decimals populated,
1137    /// and the freeze_authority COption left empty (None).
1138    fn make_mint_with_authority_decimals(
1139        mint_authority: [u8; 32],
1140        decimals: u8,
1141    ) -> (std::vec::Vec<u8>, crate::account::AccountView) {
1142        use hopper_native::{AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED};
1143
1144        let mint_data_len = 82;
1145        let mut backing = std::vec![0u8; RuntimeAccount::SIZE + mint_data_len];
1146        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
1147        unsafe {
1148            raw.write(RuntimeAccount {
1149                borrow_state: NOT_BORROWED,
1150                is_signer: 0,
1151                is_writable: 0,
1152                executable: 0,
1153                resize_delta: 0,
1154                address: NativeAddress::new_from_array([0xBB; 32]),
1155                owner: NativeAddress::new_from_array([3; 32]),
1156                lamports: 1_461_600,
1157                data_len: mint_data_len as u64,
1158            });
1159            let data_ptr = (raw as *mut u8).add(RuntimeAccount::SIZE);
1160            // mint_authority COption tag = Some (u32 LE = 1).
1161            let some_tag: [u8; 4] = 1u32.to_le_bytes();
1162            core::ptr::copy_nonoverlapping(some_tag.as_ptr(), data_ptr, 4);
1163            core::ptr::copy_nonoverlapping(mint_authority.as_ptr(), data_ptr.add(4), 32);
1164            // Supply bytes [36..44] stay zero.
1165            // Decimals at byte 44.
1166            *data_ptr.add(44) = decimals;
1167            // is_initialized byte 45 = 1.
1168            *data_ptr.add(45) = 1;
1169            // freeze_authority COption tag = None (bytes 46..50 stay zero).
1170        }
1171        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
1172        let view = crate::account::AccountView::from_backend(backend);
1173        (backing, view)
1174    }
1175
1176    #[test]
1177    fn require_token_mint_accepts_matching_mint() {
1178        let mint = [0xABu8; 32];
1179        let (_b, view) = make_token_with_mint_and_owner(mint, [0; 32]);
1180        let expected = crate::address::Address::new_from_array(mint);
1181        require_token_mint(&view, &expected).unwrap();
1182    }
1183
1184    #[test]
1185    fn require_token_mint_rejects_mismatched_mint() {
1186        let mint = [0xABu8; 32];
1187        let (_b, view) = make_token_with_mint_and_owner(mint, [0; 32]);
1188        let wrong = crate::address::Address::new_from_array([0xCDu8; 32]);
1189        let err = require_token_mint(&view, &wrong).unwrap_err();
1190        assert!(matches!(err, ProgramError::InvalidAccountData));
1191    }
1192
1193    #[test]
1194    fn require_token_owner_eq_matches() {
1195        let owner = [0x77u8; 32];
1196        let (_b, view) = make_token_with_mint_and_owner([0; 32], owner);
1197        let expected = crate::address::Address::new_from_array(owner);
1198        require_token_owner_eq(&view, &expected).unwrap();
1199    }
1200
1201    #[test]
1202    fn require_token_owner_eq_rejects_mismatch() {
1203        let owner = [0x77u8; 32];
1204        let (_b, view) = make_token_with_mint_and_owner([0; 32], owner);
1205        let wrong = crate::address::Address::new_from_array([0x88u8; 32]);
1206        let err = require_token_owner_eq(&view, &wrong).unwrap_err();
1207        assert!(matches!(err, ProgramError::IncorrectAuthority));
1208    }
1209
1210    #[test]
1211    fn require_mint_authority_accepts_matching() {
1212        let auth = [0x99u8; 32];
1213        let (_b, view) = make_mint_with_authority_decimals(auth, 6);
1214        let expected = crate::address::Address::new_from_array(auth);
1215        require_mint_authority(&view, &expected).unwrap();
1216    }
1217
1218    #[test]
1219    fn require_mint_authority_rejects_mismatched() {
1220        let auth = [0x99u8; 32];
1221        let (_b, view) = make_mint_with_authority_decimals(auth, 6);
1222        let wrong = crate::address::Address::new_from_array([0x00u8; 32]);
1223        let err = require_mint_authority(&view, &wrong).unwrap_err();
1224        assert!(matches!(err, ProgramError::IncorrectAuthority));
1225    }
1226
1227    #[test]
1228    fn require_mint_decimals_matches() {
1229        let (_b, view) = make_mint_with_authority_decimals([1u8; 32], 9);
1230        require_mint_decimals(&view, 9).unwrap();
1231    }
1232
1233    #[test]
1234    fn require_mint_decimals_rejects_mismatch() {
1235        let (_b, view) = make_mint_with_authority_decimals([1u8; 32], 9);
1236        let err = require_mint_decimals(&view, 6).unwrap_err();
1237        assert!(matches!(err, ProgramError::InvalidAccountData));
1238    }
1239
1240    #[test]
1241    fn require_mint_freeze_authority_rejects_none_tag() {
1242        // `make_mint_with_authority_decimals` deliberately leaves
1243        // freeze_authority as None. asking for a specific freeze
1244        // authority on such a mint must fail with InvalidAccountData
1245        // (not IncorrectAuthority, because the tag is the problem
1246        // rather than the pubkey bytes).
1247        let (_b, view) = make_mint_with_authority_decimals([1u8; 32], 9);
1248        let expected = crate::address::Address::new_from_array([2u8; 32]);
1249        let err = require_mint_freeze_authority(&view, &expected).unwrap_err();
1250        assert!(matches!(err, ProgramError::InvalidAccountData));
1251    }
1252}