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}