light_token_interface/state/token/
zero_copy.rs

1use core::ops::{Deref, DerefMut};
2
3use aligned_sized::aligned_sized;
4use light_compressed_account::Pubkey;
5use light_program_profiler::profile;
6use light_zero_copy::{
7    traits::{ZeroCopyAt, ZeroCopyAtMut},
8    ZeroCopy, ZeroCopyMut, ZeroCopyNew,
9};
10
11use crate::{
12    state::{
13        ExtensionStruct, ExtensionStructConfig, Token, ZExtensionStruct, ZExtensionStructMut,
14        ACCOUNT_TYPE_TOKEN_ACCOUNT,
15    },
16    AnchorDeserialize, AnchorSerialize,
17};
18
19/// SPL Token Account base size (165 bytes)
20pub const BASE_TOKEN_ACCOUNT_SIZE: u64 = TokenZeroCopyMeta::LEN as u64;
21
22/// SPL-compatible Token zero copy struct (165 bytes).
23/// Uses derive macros to generate TokenZeroCopyMeta<'a> and TokenZeroCopyMetaMut<'a>.
24/// Note: account_type byte at position 165 is handled separately in ZeroCopyAt/ZeroCopyAtMut implementations.
25#[derive(
26    Debug, PartialEq, Eq, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy, ZeroCopyMut,
27)]
28#[repr(C)]
29#[aligned_sized]
30struct TokenZeroCopyMeta {
31    /// The mint associated with this account
32    pub mint: Pubkey,
33    /// The owner of this account.
34    pub owner: Pubkey,
35    /// The amount of tokens this account holds.
36    pub amount: u64,
37    delegate_option_prefix: u32,
38    /// If `delegate` is `Some` then `delegated_amount` represents
39    /// the amount authorized by the delegate
40    delegate: Pubkey,
41    /// The account's state
42    pub state: u8,
43    /// If `is_some`, this is a native token, and the value logs the rent-exempt
44    /// reserve. An Account is required to be rent-exempt, so the value is
45    /// used by the Processor to ensure that wrapped SOL accounts do not
46    /// drop below this threshold.
47    is_native_option_prefix: u32,
48    is_native: u64,
49    /// The amount delegated
50    pub delegated_amount: u64,
51    /// Optional authority to close the account.
52    close_authority_option_prefix: u32,
53    close_authority: Pubkey,
54    // End of SPL Token Account compatible layout (165 bytes)
55}
56
57/// Zero-copy view of Token with base and optional extensions
58#[derive(Debug)]
59pub struct ZToken<'a> {
60    pub base: ZTokenZeroCopyMeta<'a>,
61    /// Account type byte read from position 165 (immutable)
62    account_type: u8,
63    pub extensions: Option<Vec<ZExtensionStruct<'a>>>,
64}
65
66/// Mutable zero-copy view of Token with base and optional extensions
67#[derive(Debug)]
68pub struct ZTokenMut<'a> {
69    pub base: ZTokenZeroCopyMetaMut<'a>,
70    /// Account type byte read from position 165 (immutable even for mut)
71    account_type: u8,
72    pub extensions: Option<Vec<ZExtensionStructMut<'a>>>,
73}
74
75/// Configuration for creating a new Token via ZeroCopyNew
76#[derive(Debug, Clone, PartialEq)]
77pub struct TokenConfig {
78    /// The mint pubkey
79    pub mint: Pubkey,
80    /// The owner pubkey
81    pub owner: Pubkey,
82    /// Account state: 1=Initialized, 2=Frozen
83    pub state: u8,
84    /// Extensions to include in the account (should include Compressible extension for compressible accounts)
85    pub extensions: Option<Vec<ExtensionStructConfig>>,
86}
87
88impl<'a> ZeroCopyNew<'a> for Token {
89    type ZeroCopyConfig = TokenConfig;
90    type Output = ZTokenMut<'a>;
91
92    fn byte_len(
93        config: &Self::ZeroCopyConfig,
94    ) -> Result<usize, light_zero_copy::errors::ZeroCopyError> {
95        let mut size = BASE_TOKEN_ACCOUNT_SIZE as usize;
96        if let Some(extensions) = &config.extensions {
97            if !extensions.is_empty() {
98                size += 1; // account_type byte at position 165
99                size += 1; // Option discriminator for extensions (1 = Some)
100                size += 4; // Vec length prefix
101                for ext in extensions {
102                    size += ExtensionStruct::byte_len(ext)?;
103                }
104            }
105        }
106        Ok(size)
107    }
108
109    fn new_zero_copy(
110        bytes: &'a mut [u8],
111        config: Self::ZeroCopyConfig,
112    ) -> Result<(Self::Output, &'a mut [u8]), light_zero_copy::errors::ZeroCopyError> {
113        // Check that the account is not already initialized (state byte at offset 108)
114        const STATE_OFFSET: usize = 108;
115        if bytes.len() > STATE_OFFSET && bytes[STATE_OFFSET] != 0 {
116            return Err(light_zero_copy::errors::ZeroCopyError::MemoryNotZeroed);
117        }
118        // Use derived new_zero_copy for base struct (config type is () for fixed-size struct)
119        let (mut base, mut remaining) =
120            <TokenZeroCopyMeta as ZeroCopyNew<'a>>::new_zero_copy(bytes, ())?;
121
122        // Set base token account fields from config
123        base.mint = config.mint;
124        base.owner = config.owner;
125        base.state = config.state;
126
127        // Write extensions using ExtensionStruct::new_zero_copy
128        let (account_type, extensions) = if let Some(ref extensions_config) = config.extensions {
129            if extensions_config.is_empty() {
130                return Err(light_zero_copy::errors::ZeroCopyError::InvalidEnumValue);
131            }
132            // Check buffer has enough space for header: account_type (1) + Option (1) + Vec len (4)
133            if remaining.len() < 6 {
134                return Err(
135                    light_zero_copy::errors::ZeroCopyError::InsufficientMemoryAllocated(
136                        remaining.len(),
137                        6,
138                    ),
139                );
140            }
141
142            // Split remaining: header (6 bytes) and extension data
143            let (header, ext_data) = remaining.split_at_mut(6);
144            // Write account_type byte at position 165
145            header[0] = ACCOUNT_TYPE_TOKEN_ACCOUNT;
146            // Write Option discriminator (1 = Some)
147            header[1] = 1;
148            // Write Vec length prefix (4 bytes, little-endian u32)
149            header[2..6].copy_from_slice(&(extensions_config.len() as u32).to_le_bytes());
150
151            // Write each extension and collect mutable references
152            let mut parsed_extensions = Vec::with_capacity(extensions_config.len());
153            let mut write_remaining = ext_data;
154
155            for ext_config in extensions_config {
156                let (ext, rest) =
157                    ExtensionStruct::new_zero_copy(write_remaining, ext_config.clone())?;
158                parsed_extensions.push(ext);
159                write_remaining = rest;
160            }
161            // Update remaining to point past all written data
162            remaining = write_remaining;
163            (ACCOUNT_TYPE_TOKEN_ACCOUNT, Some(parsed_extensions))
164        } else {
165            (ACCOUNT_TYPE_TOKEN_ACCOUNT, None)
166        };
167        if !remaining.is_empty() {
168            return Err(light_zero_copy::errors::ZeroCopyError::Size);
169        }
170        Ok((
171            ZTokenMut {
172                base,
173                account_type,
174                extensions,
175            },
176            remaining,
177        ))
178    }
179}
180
181impl<'a> ZeroCopyAt<'a> for Token {
182    type ZeroCopyAt = ZToken<'a>;
183
184    #[inline(always)]
185    fn zero_copy_at(
186        bytes: &'a [u8],
187    ) -> Result<(Self::ZeroCopyAt, &'a [u8]), light_zero_copy::errors::ZeroCopyError> {
188        let (base, bytes) = <TokenZeroCopyMeta as ZeroCopyAt<'a>>::zero_copy_at(bytes)?;
189
190        // Check if there are extensions by looking at account_type byte at position 165
191        if !bytes.is_empty() {
192            let account_type = bytes[0];
193            // Skip account_type byte
194            let bytes = &bytes[1..];
195
196            // Read extensions using Option<Vec<ExtensionStruct>>
197            let (extensions, bytes) =
198                <Option<Vec<ExtensionStruct>> as ZeroCopyAt<'a>>::zero_copy_at(bytes)?;
199            Ok((
200                ZToken {
201                    base,
202                    account_type,
203                    extensions,
204                },
205                bytes,
206            ))
207        } else {
208            // No extensions - account_type defaults to TOKEN_ACCOUNT type
209            Ok((
210                ZToken {
211                    base,
212                    account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT,
213                    extensions: None,
214                },
215                bytes,
216            ))
217        }
218    }
219}
220
221impl<'a> ZeroCopyAtMut<'a> for Token {
222    type ZeroCopyAtMut = ZTokenMut<'a>;
223
224    #[inline(always)]
225    fn zero_copy_at_mut(
226        bytes: &'a mut [u8],
227    ) -> Result<(Self::ZeroCopyAtMut, &'a mut [u8]), light_zero_copy::errors::ZeroCopyError> {
228        let (base, bytes) = <TokenZeroCopyMeta as ZeroCopyAtMut<'a>>::zero_copy_at_mut(bytes)?;
229
230        // Check if there are extensions by looking at account_type byte at position 165
231        if !bytes.is_empty() {
232            let account_type = bytes[0];
233            // Skip account_type byte
234            let bytes = &mut bytes[1..];
235
236            // Read extensions using Option<Vec<ExtensionStruct>>
237            let (extensions, bytes) =
238                <Option<Vec<ExtensionStruct>> as ZeroCopyAtMut<'a>>::zero_copy_at_mut(bytes)?;
239            Ok((
240                ZTokenMut {
241                    base,
242                    account_type,
243                    extensions,
244                },
245                bytes,
246            ))
247        } else {
248            // No extensions - account_type defaults to TOKEN_ACCOUNT type
249            Ok((
250                ZTokenMut {
251                    base,
252                    account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT,
253                    extensions: None,
254                },
255                bytes,
256            ))
257        }
258    }
259}
260
261// Deref implementations for field access
262impl<'a> Deref for ZToken<'a> {
263    type Target = ZTokenZeroCopyMeta<'a>;
264
265    fn deref(&self) -> &Self::Target {
266        &self.base
267    }
268}
269
270impl<'a> Deref for ZTokenMut<'a> {
271    type Target = ZTokenZeroCopyMetaMut<'a>;
272
273    fn deref(&self) -> &Self::Target {
274        &self.base
275    }
276}
277
278impl<'a> DerefMut for ZTokenMut<'a> {
279    fn deref_mut(&mut self) -> &mut Self::Target {
280        &mut self.base
281    }
282}
283
284// Getters on ZToken (immutable view)
285impl<'a> ZToken<'a> {
286    /// Returns the account_type byte read from position 165
287    #[inline(always)]
288    pub fn account_type(&self) -> u8 {
289        self.account_type
290    }
291
292    /// Checks if account_type matches Token discriminator value
293    #[inline(always)]
294    pub fn is_token_account(&self) -> bool {
295        self.account_type == ACCOUNT_TYPE_TOKEN_ACCOUNT
296    }
297
298    /// Returns a reference to the Compressible extension if it exists
299    #[inline(always)]
300    pub fn get_compressible_extension(
301        &self,
302    ) -> Option<&crate::state::extensions::ZCompressibleExtension<'a>> {
303        self.extensions.as_ref().and_then(|exts| {
304            exts.iter().find_map(|ext| match ext {
305                ZExtensionStruct::Compressible(comp) => Some(comp),
306                _ => None,
307            })
308        })
309    }
310}
311
312// Getters on ZTokenMut (account_type is still immutable)
313impl<'a> ZTokenMut<'a> {
314    /// Returns the account_type byte read from position 165
315    #[inline(always)]
316    pub fn account_type(&self) -> u8 {
317        self.account_type
318    }
319
320    /// Checks if account_type matches Token discriminator value
321    #[inline(always)]
322    pub fn is_token_account(&self) -> bool {
323        self.account_type == ACCOUNT_TYPE_TOKEN_ACCOUNT
324    }
325
326    /// Returns a mutable reference to the Compressible extension if it exists
327    #[inline(always)]
328    pub fn get_compressible_extension_mut(
329        &mut self,
330    ) -> Option<&mut crate::state::extensions::ZCompressibleExtensionMut<'a>> {
331        self.extensions.as_mut().and_then(|exts| {
332            exts.iter_mut().find_map(|ext| match ext {
333                ZExtensionStructMut::Compressible(comp) => Some(comp),
334                _ => None,
335            })
336        })
337    }
338
339    /// Returns an immutable reference to the Compressible extension if it exists
340    #[inline(always)]
341    pub fn get_compressible_extension(
342        &self,
343    ) -> Option<&crate::state::extensions::ZCompressibleExtensionMut<'a>> {
344        self.extensions.as_ref().and_then(|exts| {
345            exts.iter().find_map(|ext| match ext {
346                ZExtensionStructMut::Compressible(comp) => Some(comp),
347                _ => None,
348            })
349        })
350    }
351}
352
353// Getters on ZTokenZeroCopyMeta (immutable)
354impl ZTokenZeroCopyMeta<'_> {
355    /// Checks if account is uninitialized (state == 0)
356    #[inline(always)]
357    pub fn is_uninitialized(&self) -> bool {
358        self.state == 0
359    }
360
361    /// Checks if account is initialized (state == 1)
362    #[inline(always)]
363    pub fn is_initialized(&self) -> bool {
364        self.state == 1
365    }
366
367    /// Checks if account is frozen (state == 2)
368    #[inline(always)]
369    pub fn is_frozen(&self) -> bool {
370        self.state == 2
371    }
372
373    /// Get delegate if set (COption discriminator == 1)
374    #[inline(always)]
375    pub fn delegate(&self) -> Option<&Pubkey> {
376        if u32::from(self.delegate_option_prefix) == 1 {
377            Some(&self.delegate)
378        } else {
379            None
380        }
381    }
382
383    /// Get is_native value if set (COption discriminator == 1)
384    #[inline(always)]
385    pub fn is_native_value(&self) -> Option<u64> {
386        if u32::from(self.is_native_option_prefix) == 1 {
387            Some(u64::from(self.is_native))
388        } else {
389            None
390        }
391    }
392
393    /// Get close_authority if set (COption discriminator == 1)
394    #[inline(always)]
395    pub fn close_authority(&self) -> Option<&Pubkey> {
396        if u32::from(self.close_authority_option_prefix) == 1 {
397            Some(&self.close_authority)
398        } else {
399            None
400        }
401    }
402}
403
404// Getters on ZTokenZeroCopyMetaMut (mutable)
405impl ZTokenZeroCopyMetaMut<'_> {
406    /// Checks if account is uninitialized (state == 0)
407    #[inline(always)]
408    pub fn is_uninitialized(&self) -> bool {
409        self.state == 0
410    }
411
412    /// Checks if account is initialized (state == 1)
413    #[inline(always)]
414    pub fn is_initialized(&self) -> bool {
415        self.state == 1
416    }
417
418    /// Checks if account is frozen (state == 2)
419    #[inline(always)]
420    pub fn is_frozen(&self) -> bool {
421        self.state == 2
422    }
423
424    /// Get delegate if set (COption discriminator == 1)
425    #[inline(always)]
426    pub fn delegate(&self) -> Option<&Pubkey> {
427        if u32::from(self.delegate_option_prefix) == 1 {
428            Some(&self.delegate)
429        } else {
430            None
431        }
432    }
433
434    /// Get is_native value if set (COption discriminator == 1)
435    #[inline(always)]
436    pub fn is_native_value(&self) -> Option<u64> {
437        if u32::from(self.is_native_option_prefix) == 1 {
438            Some(u64::from(self.is_native))
439        } else {
440            None
441        }
442    }
443
444    /// Get close_authority if set (COption discriminator == 1)
445    #[inline(always)]
446    pub fn close_authority(&self) -> Option<&Pubkey> {
447        if u32::from(self.close_authority_option_prefix) == 1 {
448            Some(&self.close_authority)
449        } else {
450            None
451        }
452    }
453
454    /// Set delegate (Some to set, None to clear)
455    #[inline(always)]
456    pub fn set_delegate(&mut self, delegate: Option<Pubkey>) -> Result<(), crate::TokenError> {
457        match delegate {
458            Some(pubkey) => {
459                self.delegate_option_prefix.set(1);
460                self.delegate = pubkey;
461            }
462            None => {
463                self.delegate_option_prefix.set(0);
464                // Clear delegate bytes
465                self.delegate = Pubkey::default();
466            }
467        }
468        Ok(())
469    }
470
471    /// Set account as frozen (state = 2)
472    #[inline(always)]
473    pub fn set_frozen(&mut self) {
474        self.state = 2;
475    }
476
477    /// Set account as initialized/unfrozen (state = 1)
478    #[inline(always)]
479    pub fn set_initialized(&mut self) {
480        self.state = 1;
481    }
482}
483
484// Checked methods on TokenZeroCopy
485impl Token {
486    /// Zero-copy deserialization with initialization and account_type check.
487    /// Returns an error if:
488    /// - Account is uninitialized (byte 108 == 0)
489    /// - Account type is not ACCOUNT_TYPE_TOKEN_ACCOUNT (byte 165 != 2)
490    ///   Allows both Initialized (1) and Frozen (2) states.
491    #[profile]
492    #[inline(always)]
493    pub fn zero_copy_at_checked(
494        bytes: &[u8],
495    ) -> Result<(ZToken<'_>, &[u8]), crate::error::TokenError> {
496        let (token, remaining) = Token::zero_copy_at(bytes)?;
497
498        if !token.is_initialized() {
499            return Err(crate::error::TokenError::InvalidAccountState);
500        }
501        if !token.is_token_account() {
502            return Err(crate::error::TokenError::InvalidAccountType);
503        }
504
505        Ok((token, remaining))
506    }
507
508    /// Mutable zero-copy deserialization with initialization and account_type check.
509    /// Returns an error if:
510    /// - Account is uninitialized (state == 0)
511    /// - Account type is not ACCOUNT_TYPE_TOKEN_ACCOUNT
512    #[profile]
513    #[inline(always)]
514    pub fn zero_copy_at_mut_checked(
515        bytes: &mut [u8],
516    ) -> Result<(ZTokenMut<'_>, &mut [u8]), crate::error::TokenError> {
517        let (token, remaining) = Token::zero_copy_at_mut(bytes)?;
518
519        if !token.is_initialized() {
520            return Err(crate::error::TokenError::InvalidAccountState);
521        }
522        if !token.is_token_account() {
523            return Err(crate::error::TokenError::InvalidAccountType);
524        }
525
526        Ok((token, remaining))
527    }
528
529    /// Deserialize a Token from account info with validation using zero-copy.
530    ///
531    /// Checks:
532    /// 1. Account is owned by the CTOKEN program
533    /// 2. Account is initialized (state != 0)
534    /// 3. Account type is ACCOUNT_TYPE_TOKEN_ACCOUNT (byte 165 == 2)
535    /// 4. No trailing bytes after the Token structure
536    ///
537    /// Safety: The returned ZToken references the account data which is valid
538    /// for the duration of the transaction. The caller must ensure the account
539    /// is not modified through other means while this reference exists.
540    #[inline(always)]
541    pub fn from_account_info_checked<'a>(
542        account_info: &pinocchio::account_info::AccountInfo,
543    ) -> Result<ZToken<'a>, crate::error::TokenError> {
544        // 1. Check program ownership
545        if !account_info.is_owned_by(&crate::LIGHT_TOKEN_PROGRAM_ID) {
546            return Err(crate::error::TokenError::InvalidTokenOwner);
547        }
548
549        let data = account_info
550            .try_borrow_data()
551            .map_err(|_| crate::error::TokenError::BorrowFailed)?;
552
553        // Extend lifetime to 'a - safe because account data lives for transaction duration
554        let data_slice: &'a [u8] =
555            unsafe { core::slice::from_raw_parts(data.as_ptr(), data.len()) };
556
557        let (token, remaining) = Token::zero_copy_at_checked(data_slice)?;
558
559        // 4. Check no trailing bytes
560        if !remaining.is_empty() {
561            return Err(crate::error::TokenError::InvalidAccountData);
562        }
563
564        Ok(token)
565    }
566
567    /// Mutable version of from_account_info_checked.
568    /// Deserialize a Token from account info with validation using zero-copy.
569    ///
570    /// Checks:
571    /// 1. Account is owned by the CTOKEN program
572    /// 2. Account is initialized (state != 0)
573    /// 3. Account type is ACCOUNT_TYPE_TOKEN_ACCOUNT (byte 165 == 2)
574    /// 4. No trailing bytes after the Token structure
575    #[inline(always)]
576    pub fn from_account_info_mut_checked<'a>(
577        account_info: &pinocchio::account_info::AccountInfo,
578    ) -> Result<ZTokenMut<'a>, crate::error::TokenError> {
579        // 1. Check program ownership
580        if !account_info.is_owned_by(&crate::LIGHT_TOKEN_PROGRAM_ID) {
581            return Err(crate::error::TokenError::InvalidTokenOwner);
582        }
583
584        let mut data = account_info
585            .try_borrow_mut_data()
586            .map_err(|_| crate::error::TokenError::BorrowFailed)?;
587
588        // Extend lifetime to 'a - safe because account data lives for transaction duration
589        let data_slice: &'a mut [u8] =
590            unsafe { core::slice::from_raw_parts_mut(data.as_mut_ptr(), data.len()) };
591
592        let (token, remaining) = Token::zero_copy_at_mut_checked(data_slice)?;
593
594        // 4. Check no trailing bytes
595        if !remaining.is_empty() {
596            return Err(crate::error::TokenError::InvalidAccountData);
597        }
598
599        Ok(token)
600    }
601}
602
603#[cfg(feature = "test-only")]
604impl PartialEq<Token> for ZToken<'_> {
605    fn eq(&self, other: &Token) -> bool {
606        // Compare basic fields
607        if self.mint.to_bytes() != other.mint.to_bytes()
608            || self.owner.to_bytes() != other.owner.to_bytes()
609            || u64::from(self.amount) != other.amount
610            || self.state != other.state as u8
611            || u64::from(self.delegated_amount) != other.delegated_amount
612            || self.account_type != other.account_type
613        {
614            return false;
615        }
616
617        // Compare delegate
618        match (self.delegate(), &other.delegate) {
619            (Some(zc_delegate), Some(regular_delegate)) => {
620                if zc_delegate.to_bytes() != regular_delegate.to_bytes() {
621                    return false;
622                }
623            }
624            (None, None) => {}
625            _ => return false,
626        }
627
628        // Compare is_native
629        match (self.is_native_value(), &other.is_native) {
630            (Some(zc_native), Some(regular_native)) => {
631                if zc_native != *regular_native {
632                    return false;
633                }
634            }
635            (None, None) => {}
636            _ => return false,
637        }
638
639        // Compare close_authority
640        match (self.close_authority(), &other.close_authority) {
641            (Some(zc_close), Some(regular_close)) => {
642                if zc_close.to_bytes() != regular_close.to_bytes() {
643                    return false;
644                }
645            }
646            (None, None) => {}
647            _ => return false,
648        }
649
650        // Compare extensions
651        match (&self.extensions, &other.extensions) {
652            (Some(zc_extensions), Some(regular_extensions)) => {
653                if zc_extensions.len() != regular_extensions.len() {
654                    return false;
655                }
656                for (zc_ext, regular_ext) in zc_extensions.iter().zip(regular_extensions.iter()) {
657                    match (zc_ext, regular_ext) {
658                        (
659                            ZExtensionStruct::TokenMetadata(zc_tm),
660                            crate::state::extensions::ExtensionStruct::TokenMetadata(regular_tm),
661                        ) => {
662                            if zc_tm.mint.to_bytes() != regular_tm.mint.to_bytes()
663                                || zc_tm.name != regular_tm.name.as_slice()
664                                || zc_tm.symbol != regular_tm.symbol.as_slice()
665                                || zc_tm.uri != regular_tm.uri.as_slice()
666                            {
667                                return false;
668                            }
669                            if zc_tm.update_authority != regular_tm.update_authority {
670                                return false;
671                            }
672                            if zc_tm.additional_metadata.len()
673                                != regular_tm.additional_metadata.len()
674                            {
675                                return false;
676                            }
677                            for (zc_meta, regular_meta) in zc_tm
678                                .additional_metadata
679                                .iter()
680                                .zip(regular_tm.additional_metadata.iter())
681                            {
682                                if zc_meta.key != regular_meta.key.as_slice()
683                                    || zc_meta.value != regular_meta.value.as_slice()
684                                {
685                                    return false;
686                                }
687                            }
688                        }
689                        (
690                            ZExtensionStruct::PausableAccount(_),
691                            crate::state::extensions::ExtensionStruct::PausableAccount(_),
692                        ) => {
693                            // Marker extension with no data, just matching discriminant is enough
694                        }
695                        (
696                            ZExtensionStruct::PermanentDelegateAccount(_),
697                            crate::state::extensions::ExtensionStruct::PermanentDelegateAccount(_),
698                        ) => {
699                            // Marker extension with no data
700                        }
701                        (
702                            ZExtensionStruct::TransferFeeAccount(zc_tfa),
703                            crate::state::extensions::ExtensionStruct::TransferFeeAccount(
704                                regular_tfa,
705                            ),
706                        ) => {
707                            if u64::from(zc_tfa.withheld_amount) != regular_tfa.withheld_amount {
708                                return false;
709                            }
710                        }
711                        (
712                            ZExtensionStruct::TransferHookAccount(zc_tha),
713                            crate::state::extensions::ExtensionStruct::TransferHookAccount(
714                                regular_tha,
715                            ),
716                        ) => {
717                            if zc_tha.transferring != regular_tha.transferring {
718                                return false;
719                            }
720                        }
721                        (
722                            ZExtensionStruct::CompressedOnly(zc_co),
723                            crate::state::extensions::ExtensionStruct::CompressedOnly(regular_co),
724                        ) => {
725                            if u64::from(zc_co.delegated_amount) != regular_co.delegated_amount
726                                || u64::from(zc_co.withheld_transfer_fee)
727                                    != regular_co.withheld_transfer_fee
728                            {
729                                return false;
730                            }
731                        }
732                        (
733                            ZExtensionStruct::Compressible(zc_comp),
734                            crate::state::extensions::ExtensionStruct::Compressible(regular_comp),
735                        ) => {
736                            // Compare decimals
737                            let zc_decimals = if zc_comp.decimals_option == 1 {
738                                Some(zc_comp.decimals)
739                            } else {
740                                None
741                            };
742                            if zc_decimals != regular_comp.decimals() {
743                                return false;
744                            }
745                            // Compare compression_only (zero-copy has u8, regular has bool)
746                            if (zc_comp.compression_only != 0) != regular_comp.compression_only {
747                                return false;
748                            }
749                            // Compare CompressionInfo fields
750                            let zc_info = &zc_comp.info;
751                            let regular_info = &regular_comp.info;
752                            if u16::from(zc_info.config_account_version)
753                                != regular_info.config_account_version
754                            {
755                                return false;
756                            }
757                            if zc_info.compress_to_pubkey != regular_info.compress_to_pubkey {
758                                return false;
759                            }
760                            if zc_info.account_version != regular_info.account_version {
761                                return false;
762                            }
763                            if u64::from(zc_info.last_claimed_slot)
764                                != regular_info.last_claimed_slot
765                            {
766                                return false;
767                            }
768                            if u32::from(zc_info.lamports_per_write)
769                                != regular_info.lamports_per_write
770                            {
771                                return false;
772                            }
773                            if zc_info.compression_authority != regular_info.compression_authority {
774                                return false;
775                            }
776                            if zc_info.rent_sponsor != regular_info.rent_sponsor {
777                                return false;
778                            }
779                            // Compare rent_config fields
780                            if u16::from(zc_info.rent_config.base_rent)
781                                != regular_info.rent_config.base_rent
782                            {
783                                return false;
784                            }
785                            if u16::from(zc_info.rent_config.compression_cost)
786                                != regular_info.rent_config.compression_cost
787                            {
788                                return false;
789                            }
790                            if zc_info.rent_config.lamports_per_byte_per_epoch
791                                != regular_info.rent_config.lamports_per_byte_per_epoch
792                            {
793                                return false;
794                            }
795                            if zc_info.rent_config.max_funded_epochs
796                                != regular_info.rent_config.max_funded_epochs
797                            {
798                                return false;
799                            }
800                            if u16::from(zc_info.rent_config.max_top_up)
801                                != regular_info.rent_config.max_top_up
802                            {
803                                return false;
804                            }
805                        }
806                        // Unknown or unhandled extension types should panic to surface bugs early
807                        (zc_ext, regular_ext) => {
808                            panic!(
809                                "Unknown extension type comparison: ZToken extension {:?} vs Token extension {:?}",
810                                std::mem::discriminant(zc_ext),
811                                std::mem::discriminant(regular_ext)
812                            );
813                        }
814                    }
815                }
816            }
817            (None, None) => {}
818            _ => return false,
819        }
820
821        true
822    }
823}
824
825#[cfg(feature = "test-only")]
826impl PartialEq<ZToken<'_>> for Token {
827    fn eq(&self, other: &ZToken<'_>) -> bool {
828        other.eq(self)
829    }
830}