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