1use anchor_lang::{prelude::*, solana_program::system_program};
2use anchor_spl::{
3 associated_token::{
4 create, get_associated_token_address, get_associated_token_address_with_program_id, Create,
5 },
6 token_interface::{close_account, transfer_checked, CloseAccount, TransferChecked},
7};
8use typed_builder::TypedBuilder;
9
10use crate::{states::StoreWalletSigner, CoreError};
11
12pub fn is_associated_token_account_or_owner(
15 pubkey: &Pubkey,
16 owner: &Pubkey,
17 mint: &Pubkey,
18) -> bool {
19 is_associated_token_account(pubkey, owner, mint) || pubkey == owner
20}
21
22pub fn is_associated_token_account(pubkey: &Pubkey, owner: &Pubkey, mint: &Pubkey) -> bool {
24 let expected = get_associated_token_address(owner, mint);
25 expected == *pubkey
26}
27
28pub fn is_associated_token_account_with_program_id(
30 pubkey: &Pubkey,
31 owner: &Pubkey,
32 mint: &Pubkey,
33 program_id: &Pubkey,
34) -> bool {
35 let expected = get_associated_token_address_with_program_id(owner, mint, program_id);
36 expected == *pubkey
37}
38
39pub fn must_be_uninitialized<'info>(account: &impl AsRef<AccountInfo<'info>>) -> bool {
41 let info = account.as_ref();
42 *info.owner == system_program::ID
43}
44
45pub fn validate_token_account<'info>(
47 account: &impl AsRef<AccountInfo<'info>>,
48 token_program_id: &Pubkey,
49) -> Result<()> {
50 let info = account.as_ref();
51
52 require!(
53 !(info.owner == &system_program::ID && info.lamports() == 0),
54 ErrorCode::AccountNotInitialized
55 );
56
57 require_keys_eq!(
58 *info.owner,
59 *token_program_id,
60 ErrorCode::AccountOwnedByWrongProgram,
61 );
62
63 let mut data: &[u8] = &info.try_borrow_data()?;
64 anchor_spl::token_interface::TokenAccount::try_deserialize(&mut data)?;
65
66 Ok(())
67}
68
69pub fn validate_associated_token_account<'info>(
71 account: &impl AsRef<AccountInfo<'info>>,
72 expected_owner: &Pubkey,
73 expected_mint: &Pubkey,
74 token_program_id: &Pubkey,
75) -> Result<()> {
76 use anchor_spl::token::accessor;
77
78 validate_token_account(account, token_program_id)?;
79
80 let info = account.as_ref();
81
82 let mint = accessor::mint(info)?;
83 require_keys_eq!(mint, *expected_mint, ErrorCode::ConstraintTokenMint);
84
85 let owner = accessor::authority(info)?;
86 require_keys_eq!(owner, *expected_owner, ErrorCode::ConstraintTokenOwner);
87
88 require!(
89 is_associated_token_account_with_program_id(
90 info.key,
91 expected_owner,
92 expected_mint,
93 token_program_id
94 ),
95 ErrorCode::AccountNotAssociatedTokenAccount
96 );
97
98 Ok(())
99}
100
101#[derive(TypedBuilder)]
102pub struct TransferAllFromEscrowToATA<'a, 'info> {
103 store_wallet: AccountInfo<'info>,
105 store_wallet_signer: &'a StoreWalletSigner,
106 system_program: AccountInfo<'info>,
107 token_program: AccountInfo<'info>,
108 associated_token_program: AccountInfo<'info>,
109 payer: AccountInfo<'info>,
110 owner: AccountInfo<'info>,
111 mint: AccountInfo<'info>,
112 decimals: u8,
113 ata: AccountInfo<'info>,
114 escrow: AccountInfo<'info>,
115 escrow_authority: AccountInfo<'info>,
116 escrow_authority_seeds: &'a [&'a [u8]],
117 init_if_needed: bool,
118 #[builder(default)]
119 skip_owner_check: bool,
120 #[builder(default)]
121 keep_escrow: bool,
122 rent_receiver: AccountInfo<'info>,
123 should_unwrap_native: bool,
124}
125
126impl TransferAllFromEscrowToATA<'_, '_> {
127 pub(crate) fn unchecked_execute(self) -> Result<bool> {
135 if self.unwrap_native_if_needed()? {
136 return Ok(true);
137 }
138
139 let Self {
140 system_program,
141 token_program,
142 associated_token_program,
143 payer,
144 owner,
145 mint,
146 decimals,
147 ata,
148 escrow,
149 escrow_authority,
150 escrow_authority_seeds,
151 init_if_needed,
152 skip_owner_check,
153 keep_escrow,
154 rent_receiver,
155 ..
156 } = self;
157
158 let amount = anchor_spl::token::accessor::amount(&escrow)?;
159
160 if amount != 0 {
161 if must_be_uninitialized(&ata) {
162 if !init_if_needed {
163 return Ok(false);
164 }
165 create(CpiContext::new(
166 associated_token_program,
167 Create {
168 payer,
169 associated_token: ata.clone(),
170 authority: owner.clone(),
171 mint: mint.clone(),
172 system_program,
173 token_program: token_program.clone(),
174 },
175 ))?;
176 }
177
178 let Ok(ata_owner) = anchor_spl::token::accessor::authority(&ata) else {
179 msg!("the ATA is not a valid token account, skip the transfer");
180 return Ok(false);
181 };
182
183 if ata_owner != owner.key() && !skip_owner_check {
184 msg!("The ATA is not owned by the owner, skip the transfer");
185 return Ok(false);
186 }
187
188 transfer_checked(
189 CpiContext::new(
190 token_program.clone(),
191 TransferChecked {
192 from: escrow.to_account_info(),
193 to: ata.to_account_info(),
194 mint: mint.clone(),
195 authority: escrow_authority.clone(),
196 },
197 )
198 .with_signer(&[escrow_authority_seeds]),
199 amount,
200 decimals,
201 )?;
202 }
203
204 if !keep_escrow {
205 close_account(
206 CpiContext::new(
207 token_program,
208 CloseAccount {
209 account: escrow.to_account_info(),
210 destination: rent_receiver,
211 authority: escrow_authority,
212 },
213 )
214 .with_signer(&[escrow_authority_seeds]),
215 )?;
216 }
217 Ok(true)
218 }
219
220 fn unwrap_native_if_needed(&self) -> Result<bool> {
223 use anchor_lang::system_program;
224
225 let Self {
226 store_wallet,
227 store_wallet_signer,
228 token_program,
229 owner,
230 ata,
231 escrow,
232 escrow_authority,
233 escrow_authority_seeds,
234 keep_escrow,
235 rent_receiver,
236 should_unwrap_native,
237 mint,
238 system_program,
239 ..
240 } = self;
241
242 let is_native_token = *mint.key == anchor_spl::token::spl_token::native_mint::ID;
243
244 let amount = anchor_spl::token::accessor::amount(escrow)?;
245
246 if is_native_token && *should_unwrap_native && amount != 0 {
248 require!(!keep_escrow, CoreError::InvalidArgument);
250
251 require_keys_eq!(*ata.key, *owner.key, CoreError::InvalidArgument);
252 require_keys_eq!(
253 anchor_spl::token::accessor::mint(escrow)?,
254 anchor_spl::token::spl_token::native_mint::ID
255 );
256
257 let balance = escrow.lamports();
258 let rent = balance
259 .checked_sub(amount)
260 .ok_or_else(|| error!(CoreError::Internal))?;
261
262 close_account(
264 CpiContext::new(
265 token_program.clone(),
266 CloseAccount {
267 account: escrow.to_account_info(),
268 destination: store_wallet.clone(),
269 authority: escrow_authority.clone(),
270 },
271 )
272 .with_signer(&[escrow_authority_seeds]),
273 )?;
274
275 let store_wallet_seeds = store_wallet_signer.signer_seeds();
276
277 if rent_receiver.key == owner.key {
278 system_program::transfer(
280 CpiContext::new(
281 system_program.clone(),
282 system_program::Transfer {
283 from: store_wallet.clone(),
284 to: owner.clone(),
285 },
286 )
287 .with_signer(&[&store_wallet_seeds]),
288 balance,
289 )?;
290 } else {
291 system_program::transfer(
293 CpiContext::new(
294 system_program.clone(),
295 system_program::Transfer {
296 from: store_wallet.clone(),
297 to: rent_receiver.clone(),
298 },
299 )
300 .with_signer(&[&store_wallet_seeds]),
301 rent,
302 )?;
303
304 system_program::transfer(
306 CpiContext::new(
307 system_program.clone(),
308 system_program::Transfer {
309 from: store_wallet.clone(),
310 to: owner.clone(),
311 },
312 )
313 .with_signer(&[&store_wallet_seeds]),
314 amount,
315 )?;
316 }
317
318 Ok(true)
319 } else {
320 Ok(false)
321 }
322 }
323}