apl_token/
state.rs

1//! State transition types
2
3use {
4    crate::instruction::MAX_SIGNERS,
5    arch_program::{
6        program_error::ProgramError,
7        program_option::COption,
8        program_pack::{IsInitialized, Pack, Sealed},
9        pubkey::{Pubkey, PUBKEY_BYTES},
10    },
11    arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs},
12    num_enum::TryFromPrimitive,
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    /// The amount delegated
102    pub delegated_amount: u64,
103    /// Optional authority to close the account.
104    pub close_authority: COption<Pubkey>,
105}
106impl Account {
107    /// Checks if account is frozen
108    pub fn is_frozen(&self) -> bool {
109        self.state == AccountState::Frozen
110    }
111    /// Checks if a token Account's owner is the `system_program` or the
112    /// incinerator
113    pub fn is_owned_by_system_program_or_incinerator_(&self) -> bool {
114        // solana_program::system_program::check_id(&self.owner)
115        //     || solana_program::incinerator::check_id(&self.owner)
116        true
117    }
118}
119impl Sealed for Account {}
120impl IsInitialized for Account {
121    fn is_initialized(&self) -> bool {
122        self.state != AccountState::Uninitialized
123    }
124}
125impl Pack for Account {
126    const LEN: usize = 164;
127    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
128        let src = array_ref![src, 0, 164];
129        let (mint, owner, amount, delegate, state, delegated_amount, close_authority) =
130            array_refs![src, 32, 32, 8, 36, 12, 8, 36];
131        Ok(Account {
132            mint: Pubkey::from_slice(mint),
133            owner: Pubkey::from_slice(owner),
134            amount: u64::from_le_bytes(*amount),
135            delegate: unpack_coption_key(delegate)?,
136            state: AccountState::try_from_primitive(state[0])
137                .or(Err(ProgramError::InvalidAccountData))?,
138            delegated_amount: u64::from_le_bytes(*delegated_amount),
139            close_authority: unpack_coption_key(close_authority)?,
140        })
141    }
142    fn pack_into_slice(&self, dst: &mut [u8]) {
143        let dst = array_mut_ref![dst, 0, 164];
144        let (
145            mint_dst,
146            owner_dst,
147            amount_dst,
148            delegate_dst,
149            state_dst,
150            delegated_amount_dst,
151            close_authority_dst,
152        ) = mut_array_refs![dst, 32, 32, 8, 36, 12, 8, 36];
153        let &Account {
154            ref mint,
155            ref owner,
156            amount,
157            ref delegate,
158            state,
159            delegated_amount,
160            ref close_authority,
161        } = self;
162        mint_dst.copy_from_slice(mint.as_ref());
163        owner_dst.copy_from_slice(owner.as_ref());
164        *amount_dst = amount.to_le_bytes();
165        pack_coption_key(delegate, delegate_dst);
166        state_dst[0] = state as u8;
167        *delegated_amount_dst = delegated_amount.to_le_bytes();
168        pack_coption_key(close_authority, close_authority_dst);
169    }
170}
171
172/// Account state.
173#[repr(u8)]
174#[derive(Clone, Copy, Debug, Default, PartialEq, TryFromPrimitive)]
175pub enum AccountState {
176    /// Account is not yet initialized
177    #[default]
178    Uninitialized,
179    /// Account is initialized; the account owner and/or delegate may perform
180    /// permitted operations on this account
181    Initialized,
182    /// Account has been frozen by the mint freeze authority. Neither the
183    /// account owner nor the delegate are able to perform operations on
184    /// this account.
185    Frozen,
186}
187
188/// Multisignature data.
189#[repr(C)]
190#[derive(Clone, Copy, Debug, Default, PartialEq)]
191pub struct Multisig {
192    /// Number of signers required
193    pub m: u8,
194    /// Number of valid signers
195    pub n: u8,
196    /// Is `true` if this structure has been initialized
197    pub is_initialized: bool,
198    /// Signer public keys
199    pub signers: [Pubkey; MAX_SIGNERS],
200}
201impl Sealed for Multisig {}
202impl IsInitialized for Multisig {
203    fn is_initialized(&self) -> bool {
204        self.is_initialized
205    }
206}
207impl Pack for Multisig {
208    const LEN: usize = 355;
209    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
210        let src = array_ref![src, 0, 355];
211        #[allow(clippy::ptr_offset_with_cast)]
212        let (m, n, is_initialized, signers_flat) = array_refs![src, 1, 1, 1, 32 * MAX_SIGNERS];
213        let mut result = Multisig {
214            m: m[0],
215            n: n[0],
216            is_initialized: match is_initialized {
217                [0] => false,
218                [1] => true,
219                _ => return Err(ProgramError::InvalidAccountData),
220            },
221            signers: [Pubkey::from_slice(&[0u8; 32]); MAX_SIGNERS],
222        };
223        for (src, dst) in signers_flat.chunks(32).zip(result.signers.iter_mut()) {
224            *dst = Pubkey::from_slice(src);
225        }
226        Ok(result)
227    }
228    fn pack_into_slice(&self, dst: &mut [u8]) {
229        let dst = array_mut_ref![dst, 0, 355];
230        #[allow(clippy::ptr_offset_with_cast)]
231        let (m, n, is_initialized, signers_flat) = mut_array_refs![dst, 1, 1, 1, 32 * MAX_SIGNERS];
232        *m = [self.m];
233        *n = [self.n];
234        *is_initialized = [self.is_initialized as u8];
235        for (i, src) in self.signers.iter().enumerate() {
236            let dst_array = array_mut_ref![signers_flat, 32 * i, 32];
237            dst_array.copy_from_slice(src.as_ref());
238        }
239    }
240}
241
242// Helpers
243fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 36]) {
244    let (tag, body) = mut_array_refs![dst, 4, 32];
245    match src {
246        COption::Some(key) => {
247            *tag = [1, 0, 0, 0];
248            body.copy_from_slice(key.as_ref());
249        }
250        COption::None => {
251            *tag = [0; 4];
252        }
253    }
254}
255fn unpack_coption_key(src: &[u8; 36]) -> Result<COption<Pubkey>, ProgramError> {
256    let (tag, body) = array_refs![src, 4, 32];
257    match *tag {
258        [0, 0, 0, 0] => Ok(COption::None),
259        [1, 0, 0, 0] => Ok(COption::Some(Pubkey::from_slice(body))),
260        _ => Err(ProgramError::InvalidAccountData),
261    }
262}
263#[allow(dead_code)]
264fn pack_coption_u64(src: &COption<u64>, dst: &mut [u8; 12]) {
265    let (tag, body) = mut_array_refs![dst, 4, 8];
266    match src {
267        COption::Some(amount) => {
268            *tag = [1, 0, 0, 0];
269            *body = amount.to_le_bytes();
270        }
271        COption::None => {
272            *tag = [0; 4];
273        }
274    }
275}
276#[allow(dead_code)]
277fn unpack_coption_u64(src: &[u8; 12]) -> Result<COption<u64>, ProgramError> {
278    let (tag, body) = array_refs![src, 4, 8];
279    match *tag {
280        [0, 0, 0, 0] => Ok(COption::None),
281        [1, 0, 0, 0] => Ok(COption::Some(u64::from_le_bytes(*body))),
282        _ => Err(ProgramError::InvalidAccountData),
283    }
284}
285
286const SPL_TOKEN_ACCOUNT_MINT_OFFSET: usize = 0;
287const SPL_TOKEN_ACCOUNT_OWNER_OFFSET: usize = 32;
288
289/// A trait for token Account structs to enable efficiently unpacking various
290/// fields without unpacking the complete state.
291pub trait GenericTokenAccount {
292    /// Check if the account data is a valid token account
293    fn valid_account_data(account_data: &[u8]) -> bool;
294
295    /// Call after account length has already been verified to unpack the
296    /// account owner
297    fn unpack_account_owner_unchecked(account_data: &[u8]) -> &Pubkey {
298        Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_OWNER_OFFSET)
299    }
300
301    /// Call after account length has already been verified to unpack the
302    /// account mint
303    fn unpack_account_mint_unchecked(account_data: &[u8]) -> &Pubkey {
304        Self::unpack_pubkey_unchecked(account_data, SPL_TOKEN_ACCOUNT_MINT_OFFSET)
305    }
306
307    /// Call after account length has already been verified to unpack a Pubkey
308    /// at the specified offset. Panics if `account_data.len()` is less than
309    /// `PUBKEY_BYTES`
310    fn unpack_pubkey_unchecked(account_data: &[u8], offset: usize) -> &Pubkey {
311        bytemuck::from_bytes(&account_data[offset..offset + PUBKEY_BYTES])
312    }
313
314    /// Unpacks an account's owner from opaque account data.
315    fn unpack_account_owner(account_data: &[u8]) -> Option<&Pubkey> {
316        if Self::valid_account_data(account_data) {
317            Some(Self::unpack_account_owner_unchecked(account_data))
318        } else {
319            None
320        }
321    }
322
323    /// Unpacks an account's mint from opaque account data.
324    fn unpack_account_mint(account_data: &[u8]) -> Option<&Pubkey> {
325        if Self::valid_account_data(account_data) {
326            Some(Self::unpack_account_mint_unchecked(account_data))
327        } else {
328            None
329        }
330    }
331}
332
333/// The offset of state field in Account's C representation
334pub const ACCOUNT_INITIALIZED_INDEX: usize = 108;
335
336/// Check if the account data buffer represents an initialized account.
337/// This is checking the `state` (`AccountState`) field of an Account object.
338pub fn is_initialized_account(account_data: &[u8]) -> bool {
339    *account_data
340        .get(ACCOUNT_INITIALIZED_INDEX)
341        .unwrap_or(&(AccountState::Uninitialized as u8))
342        != AccountState::Uninitialized as u8
343}
344
345impl GenericTokenAccount for Account {
346    fn valid_account_data(account_data: &[u8]) -> bool {
347        account_data.len() == Account::LEN && is_initialized_account(account_data)
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_mint_unpack_from_slice() {
357        let src: [u8; 82] = [0; 82];
358        let mint = Mint::unpack_from_slice(&src).unwrap();
359        assert!(!mint.is_initialized);
360
361        let mut src: [u8; 82] = [0; 82];
362        src[45] = 2;
363        let mint = Mint::unpack_from_slice(&src).unwrap_err();
364        assert_eq!(mint, ProgramError::InvalidAccountData);
365    }
366
367    #[test]
368    fn test_account_state() {
369        let account_state = AccountState::default();
370        assert_eq!(account_state, AccountState::Uninitialized);
371    }
372
373    #[test]
374    fn test_multisig_unpack_from_slice() {
375        let src: [u8; 355] = [0; 355];
376        let multisig = Multisig::unpack_from_slice(&src).unwrap();
377        assert_eq!(multisig.m, 0);
378        assert_eq!(multisig.n, 0);
379        assert!(!multisig.is_initialized);
380
381        let mut src: [u8; 355] = [0; 355];
382        src[0] = 1;
383        src[1] = 1;
384        src[2] = 1;
385        let multisig = Multisig::unpack_from_slice(&src).unwrap();
386        assert_eq!(multisig.m, 1);
387        assert_eq!(multisig.n, 1);
388        assert!(multisig.is_initialized);
389
390        let mut src: [u8; 355] = [0; 355];
391        src[2] = 2;
392        let multisig = Multisig::unpack_from_slice(&src).unwrap_err();
393        assert_eq!(multisig, ProgramError::InvalidAccountData);
394    }
395
396    #[test]
397    fn test_unpack_coption_key() {
398        let src: [u8; 36] = [0; 36];
399        let result = unpack_coption_key(&src).unwrap();
400        assert_eq!(result, COption::None);
401
402        let mut src: [u8; 36] = [0; 36];
403        src[1] = 1;
404        let result = unpack_coption_key(&src).unwrap_err();
405        assert_eq!(result, ProgramError::InvalidAccountData);
406    }
407
408    #[test]
409    fn test_unpack_coption_u64() {
410        let src: [u8; 12] = [0; 12];
411        let result = unpack_coption_u64(&src).unwrap();
412        assert_eq!(result, COption::None);
413
414        let mut src: [u8; 12] = [0; 12];
415        src[0] = 1;
416        let result = unpack_coption_u64(&src).unwrap();
417        assert_eq!(result, COption::Some(0));
418
419        let mut src: [u8; 12] = [0; 12];
420        src[1] = 1;
421        let result = unpack_coption_u64(&src).unwrap_err();
422        assert_eq!(result, ProgramError::InvalidAccountData);
423    }
424
425    #[test]
426    fn test_unpack_token_owner() {
427        // Account data length < Account::LEN, unpack will not return a key
428        let src: [u8; 12] = [0; 12];
429        let result = Account::unpack_account_owner(&src);
430        assert_eq!(result, Option::None);
431
432        // The right account data size and initialized, unpack will return some key
433        let mut src: [u8; Account::LEN] = [0; Account::LEN];
434        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8;
435        let result = Account::unpack_account_owner(&src);
436        assert!(result.is_some());
437
438        // The right account data size and frozen, unpack will return some key
439        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8;
440        let result = Account::unpack_account_owner(&src);
441        assert!(result.is_some());
442
443        // The right account data size and uninitialized, unpack will return None
444        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8;
445        let result = Account::unpack_account_mint(&src);
446        assert_eq!(result, Option::None);
447
448        // Account data length > account data size, unpack will not return a key
449        let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5];
450        let result = Account::unpack_account_owner(&src);
451        assert_eq!(result, Option::None);
452    }
453
454    #[test]
455    fn test_unpack_token_mint() {
456        // Account data length < Account::LEN, unpack will not return a key
457        let src: [u8; 12] = [0; 12];
458        let result = Account::unpack_account_mint(&src);
459        assert_eq!(result, Option::None);
460
461        // The right account data size and initialized, unpack will return some key
462        let mut src: [u8; Account::LEN] = [0; Account::LEN];
463        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8;
464        let result = Account::unpack_account_mint(&src);
465        assert!(result.is_some());
466
467        // The right account data size and frozen, unpack will return some key
468        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8;
469        let result = Account::unpack_account_mint(&src);
470        assert!(result.is_some());
471
472        // The right account data size and uninitialized, unpack will return None
473        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8;
474        let result = Account::unpack_account_mint(&src);
475        assert_eq!(result, Option::None);
476
477        // Account data length > account data size, unpack will not return a key
478        let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5];
479        let result = Account::unpack_account_mint(&src);
480        assert_eq!(result, Option::None);
481    }
482}