spl_token/
state.rs

1//! State transition types
2
3use {
4    crate::instruction::MAX_SIGNERS,
5    arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs},
6    num_enum::TryFromPrimitive,
7    miraland_program::{
8        program_error::ProgramError,
9        program_option::COption,
10        program_pack::{IsInitialized, Pack, Sealed},
11        pubkey::{Pubkey, PUBKEY_BYTES},
12    },
13};
14
15/// Mint data.
16#[repr(C)]
17#[derive(Clone, Copy, Debug, Default, PartialEq)]
18pub struct Mint {
19    /// Optional authority used to mint new tokens. The mint authority may only
20    /// be provided during mint creation. If no mint authority is present
21    /// then the mint has a fixed supply and no further tokens may be
22    /// minted.
23    pub mint_authority: COption<Pubkey>,
24    /// Total supply of tokens.
25    pub supply: u64,
26    /// Number of base 10 digits to the right of the decimal place.
27    pub decimals: u8,
28    /// Is `true` if this structure has been initialized
29    pub is_initialized: bool,
30    /// Optional authority to freeze token accounts.
31    pub freeze_authority: COption<Pubkey>,
32}
33impl Sealed for Mint {}
34impl IsInitialized for Mint {
35    fn is_initialized(&self) -> bool {
36        self.is_initialized
37    }
38}
39impl Pack for Mint {
40    const LEN: usize = 82;
41    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
42        let src = array_ref![src, 0, 82];
43        let (mint_authority, supply, decimals, is_initialized, freeze_authority) =
44            array_refs![src, 36, 8, 1, 1, 36];
45        let mint_authority = unpack_coption_key(mint_authority)?;
46        let supply = u64::from_le_bytes(*supply);
47        let decimals = decimals[0];
48        let is_initialized = match is_initialized {
49            [0] => false,
50            [1] => true,
51            _ => return Err(ProgramError::InvalidAccountData),
52        };
53        let freeze_authority = unpack_coption_key(freeze_authority)?;
54        Ok(Mint {
55            mint_authority,
56            supply,
57            decimals,
58            is_initialized,
59            freeze_authority,
60        })
61    }
62    fn pack_into_slice(&self, dst: &mut [u8]) {
63        let dst = array_mut_ref![dst, 0, 82];
64        let (
65            mint_authority_dst,
66            supply_dst,
67            decimals_dst,
68            is_initialized_dst,
69            freeze_authority_dst,
70        ) = mut_array_refs![dst, 36, 8, 1, 1, 36];
71        let &Mint {
72            ref mint_authority,
73            supply,
74            decimals,
75            is_initialized,
76            ref freeze_authority,
77        } = self;
78        pack_coption_key(mint_authority, mint_authority_dst);
79        *supply_dst = supply.to_le_bytes();
80        decimals_dst[0] = decimals;
81        is_initialized_dst[0] = is_initialized as u8;
82        pack_coption_key(freeze_authority, freeze_authority_dst);
83    }
84}
85
86/// Account data.
87#[repr(C)]
88#[derive(Clone, Copy, Debug, Default, PartialEq)]
89pub struct Account {
90    /// The mint associated with this account
91    pub mint: Pubkey,
92    /// The owner of this account.
93    pub owner: Pubkey,
94    /// The amount of tokens this account holds.
95    pub amount: u64,
96    /// If `delegate` is `Some` then `delegated_amount` represents
97    /// the amount authorized by the delegate
98    pub delegate: COption<Pubkey>,
99    /// The account's state
100    pub state: AccountState,
101    /// If is_native.is_some, this is a native token, and the value logs the
102    /// rent-exempt reserve. An Account is required to be rent-exempt, so
103    /// the value is used by the Processor to ensure that wrapped MLN
104    /// accounts do not drop below this threshold.
105    pub is_native: COption<u64>,
106    /// The amount delegated
107    pub delegated_amount: u64,
108    /// Optional authority to close the account.
109    pub close_authority: COption<Pubkey>,
110}
111impl Account {
112    /// Checks if account is frozen
113    pub fn is_frozen(&self) -> bool {
114        self.state == AccountState::Frozen
115    }
116    /// Checks if account is native
117    pub fn is_native(&self) -> bool {
118        self.is_native.is_some()
119    }
120    /// Checks if a token Account's owner is the system_program or the
121    /// incinerator
122    pub fn is_owned_by_system_program_or_incinerator(&self) -> bool {
123        miraland_program::system_program::check_id(&self.owner)
124            || miraland_program::incinerator::check_id(&self.owner)
125    }
126}
127impl Sealed for Account {}
128impl IsInitialized for Account {
129    fn is_initialized(&self) -> bool {
130        self.state != AccountState::Uninitialized
131    }
132}
133impl Pack for Account {
134    const LEN: usize = 165;
135    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
136        let src = array_ref![src, 0, 165];
137        let (mint, owner, amount, delegate, state, is_native, delegated_amount, close_authority) =
138            array_refs![src, 32, 32, 8, 36, 1, 12, 8, 36];
139        Ok(Account {
140            mint: Pubkey::new_from_array(*mint),
141            owner: Pubkey::new_from_array(*owner),
142            amount: u64::from_le_bytes(*amount),
143            delegate: unpack_coption_key(delegate)?,
144            state: AccountState::try_from_primitive(state[0])
145                .or(Err(ProgramError::InvalidAccountData))?,
146            is_native: unpack_coption_u64(is_native)?,
147            delegated_amount: u64::from_le_bytes(*delegated_amount),
148            close_authority: unpack_coption_key(close_authority)?,
149        })
150    }
151    fn pack_into_slice(&self, dst: &mut [u8]) {
152        let dst = array_mut_ref![dst, 0, 165];
153        let (
154            mint_dst,
155            owner_dst,
156            amount_dst,
157            delegate_dst,
158            state_dst,
159            is_native_dst,
160            delegated_amount_dst,
161            close_authority_dst,
162        ) = mut_array_refs![dst, 32, 32, 8, 36, 1, 12, 8, 36];
163        let &Account {
164            ref mint,
165            ref owner,
166            amount,
167            ref delegate,
168            state,
169            ref is_native,
170            delegated_amount,
171            ref close_authority,
172        } = self;
173        mint_dst.copy_from_slice(mint.as_ref());
174        owner_dst.copy_from_slice(owner.as_ref());
175        *amount_dst = amount.to_le_bytes();
176        pack_coption_key(delegate, delegate_dst);
177        state_dst[0] = state as u8;
178        pack_coption_u64(is_native, is_native_dst);
179        *delegated_amount_dst = delegated_amount.to_le_bytes();
180        pack_coption_key(close_authority, close_authority_dst);
181    }
182}
183
184/// Account state.
185#[repr(u8)]
186#[derive(Clone, Copy, Debug, Default, PartialEq, TryFromPrimitive)]
187pub enum AccountState {
188    /// Account is not yet initialized
189    #[default]
190    Uninitialized,
191    /// Account is initialized; the account owner and/or delegate may perform
192    /// permitted operations on this account
193    Initialized,
194    /// Account has been frozen by the mint freeze authority. Neither the
195    /// account owner nor the delegate are able to perform operations on
196    /// this account.
197    Frozen,
198}
199
200/// Multisignature data.
201#[repr(C)]
202#[derive(Clone, Copy, Debug, Default, PartialEq)]
203pub struct Multisig {
204    /// Number of signers required
205    pub m: u8,
206    /// Number of valid signers
207    pub n: u8,
208    /// Is `true` if this structure has been initialized
209    pub is_initialized: bool,
210    /// Signer public keys
211    pub signers: [Pubkey; MAX_SIGNERS],
212}
213impl Sealed for Multisig {}
214impl IsInitialized for Multisig {
215    fn is_initialized(&self) -> bool {
216        self.is_initialized
217    }
218}
219impl Pack for Multisig {
220    const LEN: usize = 355;
221    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
222        let src = array_ref![src, 0, 355];
223        #[allow(clippy::ptr_offset_with_cast)]
224        let (m, n, is_initialized, signers_flat) = array_refs![src, 1, 1, 1, 32 * MAX_SIGNERS];
225        let mut result = Multisig {
226            m: m[0],
227            n: n[0],
228            is_initialized: match is_initialized {
229                [0] => false,
230                [1] => true,
231                _ => return Err(ProgramError::InvalidAccountData),
232            },
233            signers: [Pubkey::new_from_array([0u8; 32]); MAX_SIGNERS],
234        };
235        for (src, dst) in signers_flat.chunks(32).zip(result.signers.iter_mut()) {
236            // *dst = Pubkey::try_from(src).expect("Slice must be the same length as a Pubkey"); // MI
237            *dst = Pubkey::try_from(src).map_err(|_| ProgramError::InvalidAccountData)?;
238        }
239        Ok(result)
240    }
241    fn pack_into_slice(&self, dst: &mut [u8]) {
242        let dst = array_mut_ref![dst, 0, 355];
243        #[allow(clippy::ptr_offset_with_cast)]
244        let (m, n, is_initialized, signers_flat) = mut_array_refs![dst, 1, 1, 1, 32 * MAX_SIGNERS];
245        *m = [self.m];
246        *n = [self.n];
247        *is_initialized = [self.is_initialized as u8];
248        for (i, src) in self.signers.iter().enumerate() {
249            let dst_array = array_mut_ref![signers_flat, 32 * i, 32];
250            dst_array.copy_from_slice(src.as_ref());
251        }
252    }
253}
254
255// Helpers
256fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 36]) {
257    let (tag, body) = mut_array_refs![dst, 4, 32];
258    match src {
259        COption::Some(key) => {
260            *tag = [1, 0, 0, 0];
261            body.copy_from_slice(key.as_ref());
262        }
263        COption::None => {
264            *tag = [0; 4];
265        }
266    }
267}
268fn unpack_coption_key(src: &[u8; 36]) -> Result<COption<Pubkey>, ProgramError> {
269    let (tag, body) = array_refs![src, 4, 32];
270    match *tag {
271        [0, 0, 0, 0] => Ok(COption::None),
272        [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))),
273        _ => Err(ProgramError::InvalidAccountData),
274    }
275}
276fn pack_coption_u64(src: &COption<u64>, dst: &mut [u8; 12]) {
277    let (tag, body) = mut_array_refs![dst, 4, 8];
278    match src {
279        COption::Some(amount) => {
280            *tag = [1, 0, 0, 0];
281            *body = amount.to_le_bytes();
282        }
283        COption::None => {
284            *tag = [0; 4];
285        }
286    }
287}
288fn unpack_coption_u64(src: &[u8; 12]) -> Result<COption<u64>, ProgramError> {
289    let (tag, body) = array_refs![src, 4, 8];
290    match *tag {
291        [0, 0, 0, 0] => Ok(COption::None),
292        [1, 0, 0, 0] => Ok(COption::Some(u64::from_le_bytes(*body))),
293        _ => Err(ProgramError::InvalidAccountData),
294    }
295}
296
297const SPL_TOKEN_ACCOUNT_MINT_OFFSET: usize = 0;
298const SPL_TOKEN_ACCOUNT_OWNER_OFFSET: usize = 32;
299
300/// A trait for token Account structs to enable efficiently unpacking various
301/// fields without unpacking the complete state.
302pub trait GenericTokenAccount {
303    /// Check if the account data is a valid token account
304    fn valid_account_data(account_data: &[u8]) -> bool;
305
306    /// Call after account length has already been verified to unpack the
307    /// account owner
308    fn unpack_account_owner_unchecked(account_data: &[u8]) -> &Pubkey {
309        Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_OWNER_OFFSET)
310    }
311
312    /// Call after account length has already been verified to unpack the
313    /// account mint
314    fn unpack_account_mint_unchecked(account_data: &[u8]) -> &Pubkey {
315        Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_MINT_OFFSET)
316    }
317
318    /// Call after account length has already been verified to unpack a Pubkey
319    /// at the specified offset. Panics if `account_data.len()` is less than
320    /// `PUBKEY_BYTES`
321    fn unpack_pubkey_unchecked(account_data: &[u8], offset: usize) -> &Pubkey {
322        bytemuck::from_bytes(&account_data[offset..offset + PUBKEY_BYTES])
323    }
324
325    /// Unpacks an account's owner from opaque account data.
326    fn unpack_account_owner(account_data: &[u8]) -> Option<&Pubkey> {
327        if Self::valid_account_data(account_data) {
328            Some(Self::unpack_account_owner_unchecked(account_data))
329        } else {
330            None
331        }
332    }
333
334    /// Unpacks an account's mint from opaque account data.
335    fn unpack_account_mint(account_data: &[u8]) -> Option<&Pubkey> {
336        if Self::valid_account_data(account_data) {
337            Some(Self::unpack_account_mint_unchecked(account_data))
338        } else {
339            None
340        }
341    }
342}
343
344/// The offset of state field in Account's C representation
345pub const ACCOUNT_INITIALIZED_INDEX: usize = 108;
346
347/// Check if the account data buffer represents an initialized account.
348/// This is checking the `state` (AccountState) field of an Account object.
349pub fn is_initialized_account(account_data: &[u8]) -> bool {
350    *account_data
351        .get(ACCOUNT_INITIALIZED_INDEX)
352        .unwrap_or(&(AccountState::Uninitialized as u8))
353        != AccountState::Uninitialized as u8
354}
355
356impl GenericTokenAccount for Account {
357    fn valid_account_data(account_data: &[u8]) -> bool {
358        account_data.len() == Account::LEN && is_initialized_account(account_data)
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_mint_unpack_from_slice() {
368        let src: [u8; 82] = [0; 82];
369        let mint = Mint::unpack_from_slice(&src).unwrap();
370        assert!(!mint.is_initialized);
371
372        let mut src: [u8; 82] = [0; 82];
373        src[45] = 2;
374        let mint = Mint::unpack_from_slice(&src).unwrap_err();
375        assert_eq!(mint, ProgramError::InvalidAccountData);
376    }
377
378    #[test]
379    fn test_account_state() {
380        let account_state = AccountState::default();
381        assert_eq!(account_state, AccountState::Uninitialized);
382    }
383
384    #[test]
385    fn test_multisig_unpack_from_slice() {
386        let src: [u8; 355] = [0; 355];
387        let multisig = Multisig::unpack_from_slice(&src).unwrap();
388        assert_eq!(multisig.m, 0);
389        assert_eq!(multisig.n, 0);
390        assert!(!multisig.is_initialized);
391
392        let mut src: [u8; 355] = [0; 355];
393        src[0] = 1;
394        src[1] = 1;
395        src[2] = 1;
396        let multisig = Multisig::unpack_from_slice(&src).unwrap();
397        assert_eq!(multisig.m, 1);
398        assert_eq!(multisig.n, 1);
399        assert!(multisig.is_initialized);
400
401        let mut src: [u8; 355] = [0; 355];
402        src[2] = 2;
403        let multisig = Multisig::unpack_from_slice(&src).unwrap_err();
404        assert_eq!(multisig, ProgramError::InvalidAccountData);
405    }
406
407    #[test]
408    fn test_unpack_coption_key() {
409        let src: [u8; 36] = [0; 36];
410        let result = unpack_coption_key(&src).unwrap();
411        assert_eq!(result, COption::None);
412
413        let mut src: [u8; 36] = [0; 36];
414        src[1] = 1;
415        let result = unpack_coption_key(&src).unwrap_err();
416        assert_eq!(result, ProgramError::InvalidAccountData);
417    }
418
419    #[test]
420    fn test_unpack_coption_u64() {
421        let src: [u8; 12] = [0; 12];
422        let result = unpack_coption_u64(&src).unwrap();
423        assert_eq!(result, COption::None);
424
425        let mut src: [u8; 12] = [0; 12];
426        src[0] = 1;
427        let result = unpack_coption_u64(&src).unwrap();
428        assert_eq!(result, COption::Some(0));
429
430        let mut src: [u8; 12] = [0; 12];
431        src[1] = 1;
432        let result = unpack_coption_u64(&src).unwrap_err();
433        assert_eq!(result, ProgramError::InvalidAccountData);
434    }
435
436    #[test]
437    fn test_unpack_token_owner() {
438        // Account data length < Account::LEN, unpack will not return a key
439        let src: [u8; 12] = [0; 12];
440        let result = Account::unpack_account_owner(&src);
441        assert_eq!(result, Option::None);
442
443        // The right account data size and initialized, unpack will return some key
444        let mut src: [u8; Account::LEN] = [0; Account::LEN];
445        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8;
446        let result = Account::unpack_account_owner(&src);
447        assert!(result.is_some());
448
449        // The right account data size and frozen, unpack will return some key
450        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8;
451        let result = Account::unpack_account_owner(&src);
452        assert!(result.is_some());
453
454        // The right account data size and uninitialized, unpack will return None
455        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8;
456        let result = Account::unpack_account_mint(&src);
457        assert_eq!(result, Option::None);
458
459        // Account data length > account data size, unpack will not return a key
460        let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5];
461        let result = Account::unpack_account_owner(&src);
462        assert_eq!(result, Option::None);
463    }
464
465    #[test]
466    fn test_unpack_token_mint() {
467        // Account data length < Account::LEN, unpack will not return a key
468        let src: [u8; 12] = [0; 12];
469        let result = Account::unpack_account_mint(&src);
470        assert_eq!(result, Option::None);
471
472        // The right account data size and initialized, unpack will return some key
473        let mut src: [u8; Account::LEN] = [0; Account::LEN];
474        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8;
475        let result = Account::unpack_account_mint(&src);
476        assert!(result.is_some());
477
478        // The right account data size and frozen, unpack will return some key
479        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8;
480        let result = Account::unpack_account_mint(&src);
481        assert!(result.is_some());
482
483        // The right account data size and uninitialized, unpack will return None
484        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8;
485        let result = Account::unpack_account_mint(&src);
486        assert_eq!(result, Option::None);
487
488        // Account data length > account data size, unpack will not return a key
489        let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5];
490        let result = Account::unpack_account_mint(&src);
491        assert_eq!(result, Option::None);
492    }
493}