Skip to main content

sealevel_tools/cpi/token_program/
create_token_account.rs

1use core::mem::size_of;
2
3use solana_program_pack::Pack;
4
5use crate::{
6    account_info::{is_any_token_program_id, Account},
7    cpi::{system_program::CreateAccount, CpiAuthority, CpiInstruction},
8    entrypoint::NoStdAccountInfo,
9    program_error::ProgramError,
10    pubkey::Pubkey,
11    spl_token_2022::{
12        extension::{
13            non_transferable::NonTransferable, transfer_fee::TransferFeeConfig,
14            transfer_hook::TransferHook, BaseStateWithExtensions, PodStateWithExtensions,
15        },
16        pod::PodMint,
17    },
18};
19
20use super::EMPTY_EXTENSION_LEN;
21
22/// Arguments to create a token account for a specific mint with a specified owner. This method
23/// creates an account for one of the Token programs and initializes it as a token account.
24///
25/// ### Example
26///
27/// ```
28/// use sealevel_tools::{
29///     account_info::{
30///         try_next_enumerated_account, try_next_enumerated_account_info, TokenProgram,
31///         AccountInfoConstraints, Payer, WritableAccount,
32///     },
33///     cpi::token_program::CreateTokenAccount,
34///     entrypoint::{NoStdAccountInfo, ProgramResult},
35///     pubkey::Pubkey,
36/// };
37///
38/// fn process_instruction(
39///      program_id: &Pubkey,
40///      accounts: &[NoStdAccountInfo],
41///      instruction_data: &[u8],
42/// ) -> ProgramResult {
43///     let mut accounts_iter = accounts.iter().enumerate();
44///
45///     // Next account must writable signer (A.K.A. our payer).
46///     let (_, payer) =
47///         try_next_enumerated_account::<Payer>(&mut accounts_iter, Default::default())?;
48///
49///     let (new_token_addr, new_token_bump) =
50///         Pubkey::find_program_address(&[b"token"], program_id);
51///
52///     // Next account must be writable data account matching PDA address.
53///     let (_, new_token_account) = try_next_enumerated_account::<WritableAccount>(
54///         &mut accounts_iter,
55///         AccountInfoConstraints {
56///             key: Some(&new_token_addr),
57///             ..Default::default()
58///         },
59///     )?;
60///
61///     // Next account must be a mint account. If you do not want to enforce that this is a read-
62///     // only account, use `try_next_enumerated_account_info` instead.
63///     let (_, mint_account) = try_next_enumerated_account::<WritableAccount>(
64///         &mut accounts_iter,
65///         Default::default()
66///     )?;
67///
68///     // Next account is the owner.
69///     let (_, token_account_owner) =
70///         try_next_enumerated_account_info(&mut accounts_iter, Default::default())?;
71///
72///     // Next account is which token program to use.
73///     let (_, token_program) =
74///         try_next_enumerated_account::<TokenProgram>(&mut accounts_iter, Default::default())?;
75///
76///     CreateTokenAccount {
77///         payer: payer.as_cpi_authority(),
78///         token_account: new_token_account.as_cpi_authority(Some(&[b"token", &[new_token_bump]])),
79///         mint: &mint_account,
80///         token_account_owner: token_account_owner.key(),
81///         immutable_owner: true,
82///     }
83///     .try_into_invoke()?;
84///
85///     Ok(())
86/// }
87/// ```
88#[derive(Clone, PartialEq, Eq)]
89pub struct CreateTokenAccount<'a, 'b: 'a> {
90    pub payer: CpiAuthority<'a, 'b>,
91    pub token_account: CpiAuthority<'a, 'b>,
92    pub mint: &'b NoStdAccountInfo,
93    pub token_account_owner: &'a Pubkey,
94    pub immutable_owner: bool,
95}
96
97impl<'a, 'b: 'a> CreateTokenAccount<'a, 'b> {
98    /// Try to consume arguments to perform CPI calls.
99    #[inline(always)]
100    pub fn try_into_invoke(self) -> Result<Account<'a, true>, ProgramError> {
101        let Self {
102            payer,
103            token_account,
104            mint,
105            token_account_owner,
106            immutable_owner,
107        } = self;
108
109        let token_program_id = mint.owner();
110
111        // Do any of these mint extensions exist? If so, need to allocate enough space for the
112        // token account counterparts.
113        let (has_transfer_fee, has_non_transferable, has_transfer_hook) = {
114            let mint_data = mint.try_borrow_data()?;
115            let mint_state = PodStateWithExtensions::<PodMint>::unpack(&mint_data)?;
116
117            (
118                mint_state.get_extension::<TransferFeeConfig>().is_ok(),
119                mint_state.get_extension::<NonTransferable>().is_ok(),
120                mint_state.get_extension::<TransferHook>().is_ok(),
121            )
122        };
123
124        let token_account =
125            if has_transfer_fee || has_non_transferable || has_transfer_hook || immutable_owner {
126                if token_program_id != &spl_token_2022::ID {
127                    return Err(super::ERROR_EXTENSIONS_UNSUPPORTED.into());
128                }
129
130                // Add to this depending on which extensions exist on the mint.
131                let mut total_space = super::BASE_WITH_EXTENSIONS_LEN;
132
133                if has_transfer_fee {
134                    total_space += {
135                        EMPTY_EXTENSION_LEN // type + length
136                        + size_of::<u64>() // withheld_amount
137                    };
138                }
139                // Non-transferable accounts have both non-transferable and immutable owner.
140                if has_non_transferable {
141                    total_space += 2 * EMPTY_EXTENSION_LEN;
142                } else if immutable_owner {
143                    total_space += EMPTY_EXTENSION_LEN;
144                }
145                if has_transfer_hook {
146                    total_space += {
147                        EMPTY_EXTENSION_LEN // type + length
148                        + size_of::<bool>() // transferring
149                    };
150                }
151
152                // Create the token account by assigning it to the token program.
153                let token_account = CreateAccount {
154                    payer,
155                    to: token_account,
156                    program_id: token_program_id,
157                    space: Some(total_space),
158                    lamports: None,
159                }
160                .try_into_invoke()?;
161
162                if !has_non_transferable && immutable_owner {
163                    super::extensions::InitializeImmutableOwner {
164                        token_program_id,
165                        account: &token_account,
166                    }
167                    .into_invoke();
168                }
169
170                token_account
171            } else {
172                if !is_any_token_program_id(token_program_id) {
173                    return Err(super::ERROR_EXPECTED_TOKEN_PROGRAM.into());
174                }
175
176                // Create the token account by assigning it to the token program.
177                CreateAccount {
178                    payer,
179                    to: token_account,
180                    program_id: token_program_id,
181                    space: Some(spl_token_2022::state::Account::LEN),
182                    lamports: None,
183                }
184                .try_into_invoke()?
185            };
186
187        _invoke_initialize_account3(token_program_id, &token_account, mint, token_account_owner);
188
189        Ok(token_account)
190    }
191}
192
193/// Arguments for the initialize token account instruction (version 3), which initializes a token
194/// account for one of the Token programs. Only use this instruction if you have already created the
195/// token account via the System program.
196///
197/// ### Notes
198///
199/// It is preferred to use [CreateTokenAccount] instead of this method because it will create
200/// the account and initialize it as a token account in one action.
201#[derive(Clone, PartialEq, Eq)]
202pub struct InitializeAccount<'a> {
203    pub token_program_id: &'a Pubkey,
204    pub account: &'a NoStdAccountInfo,
205    pub mint: &'a NoStdAccountInfo,
206    pub owner: &'a Pubkey,
207}
208
209impl<'a> InitializeAccount<'a> {
210    /// Consume arguments to perform CPI call.
211    #[inline(always)]
212    pub fn into_invoke(self) {
213        let Self {
214            token_program_id,
215            account,
216            mint,
217            owner,
218        } = self;
219
220        _invoke_initialize_account3(token_program_id, account, mint, owner);
221    }
222}
223
224#[inline(always)]
225fn _invoke_initialize_account3(
226    token_program_id: &Pubkey,
227    account: &NoStdAccountInfo,
228    mint: &NoStdAccountInfo,
229    owner: &Pubkey,
230) {
231    // Initialize account 3 selector == 18.
232    let instruction_data = super::serialize_authority_instruction_data(18, owner);
233
234    CpiInstruction {
235        program_id: token_program_id,
236        accounts: &[account.to_meta_c(), mint.to_meta_c()],
237        data: &instruction_data,
238    }
239    .invoke_signed(&[account.to_info_c(), mint.to_info_c()], &[]);
240}