light_token_interface/state/mint/
compressed_mint.rs

1use borsh::{BorshDeserialize, BorshSerialize};
2use light_compressed_account::{address::derive_address, Pubkey};
3use light_compressible::compression_info::CompressionInfo;
4use light_hasher::{sha256::Sha256BE, Hasher};
5use light_zero_copy::{ZeroCopy, ZeroCopyMut};
6use pinocchio::account_info::AccountInfo;
7#[cfg(feature = "solana")]
8use solana_msg::msg;
9
10use crate::{
11    state::ExtensionStruct, AnchorDeserialize, AnchorSerialize, TokenError, LIGHT_TOKEN_PROGRAM_ID,
12    MINT_ADDRESS_TREE,
13};
14
15/// AccountType::Mint discriminator value
16pub const ACCOUNT_TYPE_MINT: u8 = 1;
17
18#[repr(C)]
19#[derive(Debug, PartialEq, Eq, Clone, BorshSerialize, BorshDeserialize)]
20pub struct Mint {
21    pub base: BaseMint,
22    pub metadata: MintMetadata,
23    /// Reserved bytes (16 bytes) for T22 layout compatibility.
24    /// Positions `account_type` at offset 165: 82 (BaseMint) + 67 (metadata) + 16 (reserved) = 165.
25    pub reserved: [u8; 16],
26    /// Account type discriminator at byte offset 165 (1 = Mint, 2 = Account)
27    pub account_type: u8,
28    /// Compression info embedded directly in the mint
29    pub compression: CompressionInfo,
30    pub extensions: Option<Vec<ExtensionStruct>>,
31}
32
33impl Default for Mint {
34    fn default() -> Self {
35        Self {
36            base: BaseMint::default(),
37            metadata: MintMetadata::default(),
38            reserved: [0u8; 16],
39            account_type: ACCOUNT_TYPE_MINT,
40            compression: CompressionInfo::default(),
41            extensions: None,
42        }
43    }
44}
45
46// and subsequent deserialization for remaining data (compression metadata + extensions)
47/// SPL-compatible base mint structure with padding for COption alignment
48#[repr(C)]
49#[derive(Debug, Default, PartialEq, Eq, Clone)]
50pub struct BaseMint {
51    /// Optional authority used to mint new tokens. The mint authority may only
52    /// be provided during mint creation. If no mint authority is present
53    /// then the mint has a fixed supply and no further tokens may be
54    /// minted.
55    pub mint_authority: Option<Pubkey>,
56    /// Total supply of tokens.
57    pub supply: u64,
58    /// Number of base 10 digits to the right of the decimal place.
59    pub decimals: u8,
60    /// Is initialized - for SPL compatibility
61    pub is_initialized: bool,
62    /// Optional authority to freeze token accounts.
63    pub freeze_authority: Option<Pubkey>,
64}
65
66/// Light Protocol-specific metadata for compressed mints.
67///
68/// Total size: 67 bytes
69/// - version: 1 byte
70/// - mint_decompressed: 1 byte
71/// - mint: 32 bytes
72/// - mint_signer: 32 bytes
73/// - bump: 1 byte
74#[repr(C)]
75#[derive(
76    Debug, Default, PartialEq, Eq, Clone, AnchorDeserialize, AnchorSerialize, ZeroCopyMut, ZeroCopy,
77)]
78pub struct MintMetadata {
79    /// Version for upgradability
80    pub version: u8,
81    /// Whether the compressed mint has been decompressed to a Mint Solana account.
82    /// When true, the Mint account is the source of truth.
83    pub mint_decompressed: bool,
84    /// PDA derived from mint_signer, used as seed for the compressed address
85    pub mint: Pubkey,
86    /// Signer pubkey used to derive the mint PDA
87    pub mint_signer: [u8; 32],
88    /// Bump seed from mint PDA derivation
89    pub bump: u8,
90}
91
92impl MintMetadata {
93    /// Derives the compressed address from mint PDA, MINT_ADDRESS_TREE and LIGHT_TOKEN_PROGRAM_ID
94    pub fn compressed_address(&self) -> [u8; 32] {
95        derive_address(
96            self.mint.array_ref(),
97            &MINT_ADDRESS_TREE,
98            &LIGHT_TOKEN_PROGRAM_ID,
99        )
100    }
101}
102
103impl ZMintMetadata<'_> {
104    /// Derives the compressed address from mint PDA, MINT_ADDRESS_TREE and LIGHT_TOKEN_PROGRAM_ID
105    pub fn compressed_address(&self) -> [u8; 32] {
106        derive_address(
107            self.mint.array_ref(),
108            &MINT_ADDRESS_TREE,
109            &LIGHT_TOKEN_PROGRAM_ID,
110        )
111    }
112}
113
114impl Mint {
115    pub fn hash(&self) -> Result<[u8; 32], TokenError> {
116        match self.metadata.version {
117            3 => Ok(Sha256BE::hash(
118                self.try_to_vec()
119                    .map_err(|_| TokenError::BorshFailed)?
120                    .as_slice(),
121            )?),
122            _ => Err(TokenError::InvalidTokenDataVersion),
123        }
124    }
125
126    /// Deserialize a Mint from a Solana account with validation.
127    ///
128    /// Checks:
129    /// 1. Account is owned by the specified program
130    /// 2. Account is initialized (BaseMint.is_initialized == true)
131    ///
132    /// Note: Mint accounts follow SPL token mint pattern (no discriminator).
133    /// Validation is done via owner check + PDA derivation (caller responsibility).
134    pub fn from_account_info_checked(account_info: &AccountInfo) -> Result<Self, TokenError> {
135        // 1. Check program ownership
136        if !account_info.is_owned_by(&LIGHT_TOKEN_PROGRAM_ID) {
137            #[cfg(feature = "solana")]
138            msg!("Mint account has invalid owner");
139            return Err(TokenError::InvalidMintOwner);
140        }
141
142        // 2. Borrow and deserialize account data
143        let data = account_info
144            .try_borrow_data()
145            .map_err(|_| TokenError::MintBorrowFailed)?;
146
147        let mint =
148            Self::try_from_slice(&data).map_err(|_| TokenError::MintDeserializationFailed)?;
149
150        // 3. Check is_initialized
151        if !mint.base.is_initialized {
152            #[cfg(feature = "solana")]
153            msg!("Mint account is not initialized");
154            return Err(TokenError::MintNotInitialized);
155        }
156
157        if !mint.is_mint_account() {
158            #[cfg(feature = "solana")]
159            msg!("Mint account is not a Mint account");
160            return Err(TokenError::MintMismatch);
161        }
162
163        Ok(mint)
164    }
165
166    /// Checks if account_type matches Mint discriminator value
167    #[inline(always)]
168    pub fn is_mint_account(&self) -> bool {
169        self.account_type == ACCOUNT_TYPE_MINT
170    }
171}