gmsol_store/utils/
token.rs

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
12/// Check if the given `pubkey` is an ATA address or
13/// the `owner` itself.
14pub 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
22/// Check if the given `pubkey` is an ATA address.
23pub 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
28/// Check if the given address is an ATA address.
29pub 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
39/// Return whether the token account must be uninitialized.
40pub 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
45/// Validate token account.
46pub 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
69/// Validate associated token account.
70pub 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 account, must be mutable.
104    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    /// Transfer all tokens from the escrow account to ATA. Close the escrow account after
128    /// the transfer is complete if `keep_escrow` is `false`, which is the default.
129    ///
130    /// Return `false` if the transfer is required but the ATA is not initilaized.
131    ///
132    /// # CHECK
133    /// - The `action` account must be owned by the store program and mutable.
134    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    /// Unwrap native if needed.
221    /// Returns `true` if unwrapped.
222    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        // Unwrap native.
247        if is_native_token && *should_unwrap_native && amount != 0 {
248            // The escrow will be closed after unwrap.
249            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            // We use the `store_wallet` account as an intermediary to distribute funds.
263            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                // Refund `balance` to the `owner`.
279                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                // Refund `rent` to the `rent_receiver`.
292                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                // Refund `amount` to the `owner`.
305                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}